typescript - Vanskelige typingsproblemer med en generisk unionstype som gjør en interseksjonstype
Jeg har snublet over et merkelig problem, og jeg har ingen anelse om hvordan jeg skal fikse dette. Jeg har en klasse med en generisk type. Denne klassen inneholder en metode med kun én union-parameter som spør etter den generiske typen eller et objekt. Siden denne klassen instansieres flere ganger med ulike typer, ønsket jeg å lage en generisk metode som henter en av disse instansene (generisk typet), og gjøre et kall av metoden innenfor.
Her er en kode for reproduksjon:
export interface A {
a: any;
}
export interface B {
b: any;
}
export interface MyClasses {
a: MyClass<A>;
b: MyClass<B>;
}
export interface C {
c: any;
}
export declare class MyClass<T = { [prop: string]: any }> {
myMethod(data: T | C): T;
}
export type MyClassesKeys = keyof MyClasses;
export type MyClassInferGenericType<T> = T extends MyClass<infer G> ? G : T;
export class MyService {
private myClasses: MyClasses = {
a: new MyClass<A>(),
b: new MyClass<B>()
};
public getMyClass<T extends MyClassesKeys>(keyName: T): MyClasses[T] {
return this.myClasses[keyName];
}
public callerMethod<T extends MyClassesKeys, U extends MyClassInferGenericType<MyClasses[T]>>(key: T, myData: U) {
// This part is usually retrieved gener
const myClassInstance = this.getMyClass(key);
// My call
myClassInstance.myMethod(myData);
}
}
Dette gir, på en eller annen måte, en kompilasjonsfeil:
Argument of type 'A | B' is not assignable to parameter of type '(A | C) & (B | C)'.
Type 'A' is not assignable to type '(A | C) & (B | C)'.
Type 'A' is not assignable to type 'A & C'.
Property 'c' is missing in type 'A' but required in type 'C'.
Med "myClassInstance" som er av typen MyClasses[T] og myData som tar den generiske typen tilknyttet MyClasses[T], bør alt bli korrekt utledet.
Så, hvorfor prøver TypeScript å lage en snittmengde av typene mine?
Svar #1
Grunnen til at du får en skjæring er fordi kompilatoren mister oversikten over korrelasjonen mellom typen til key og typen til myData, og derfor aksepterer den bare noe den vet er trygt. Hvis myData var både A og B (som {a: 1, b: 2}), ville det være trygt å kalle myClassInstance.myMethod(myData) uansett hvilken key som ble sendt inn.
Det er det som skjer fordi myClassInstance.myMethod blir sett på som å ha en unionstype:
const x = myClassInstance.myMethod;
// const x: ((data: A | C) => A) | ((data: B | C) => B)
Og hvis du kaller en forening av funksjoner, må du sende inn et snitt av dens parametere for at kompilatoren skal godta det.
Selvfølgelig er det umulig for noen å sende inn myData og key som ikke er riktig matchet. (Egentlig er ikke det sant, siden key kan være av en unionstype. Uten ms/TS#27808 er det mulig. La oss si at det er mulig, men usannsynlig. Eller i det minste at vi ikke vil bekymre oss for det her.) Men kompilatoren kan ikke se dette.
Det finnes egentlig ikke støtte for at kompilatoren kan se vilkårlig komplekse korrelasjoner mellom forskjellige typer. Det grunnleggende problemet er beskrevet i microsoft/TypeScript#30581, som rammer det inn i form av korrelerte unions.
Heldigvis finnes det ofte en måte å refaktorere typene på slik at kompilatoren er i stand til å følge logikken. Dette er beskrevet i microsoft/TypeScript#47109. Ideen er å lage en «grunnleggende» type som representerer forholdet mellom inn- og utdata så enkelt som mulig:
interface MyClassGenerics {
a: A;
b: B
}
Og så skriver vi MyClasses eksplisitt som en kartlagt type over den grunnleggende typen:
type MyClasses = {
[K in keyof MyClassGenerics]: MyClass<MyClassGenerics[K]>
}
Så nå, når du skriver MyClasses[K], vil kompilatoren automatisk se dets forhold til MyClass<MyClassGenerics[K]>. Og du kan referere til MyClassGenerics[K] uten å måtte bruke MyClassInferGenericType:
public callerMethod<K extends MyClassesKeys>(key: K, myData: MyClassGenerics[K]) {
const myClassInstance = this.getMyClass(key);
const x = myClassInstance.myMethod
// const x: (data: C | MyClassGenerics[K]) => MyClassGenerics[K]
myClassInstance.myMethod(myData); // okay
}
Nå blir typen til myClassInstance.myMethod sett på som en enkelt funksjonstype hvis inngangs- og utgangstyper avhenger av den generiske typeparameteren K. Den er ikke lenger en union, så den kan kalles lettere.
Lenke til kode på lekeplassen
