Låt TypeScript härleda generiskt argument
Jag har en basklass och en härledd typ
interface IInferredTypeBase {
}
interface IInferredType extends IInferredTypeBase {
myProperty;
}
I verkligheten finns det många efterkommande i den hierarkin.
Nu har jag också en typ som är generisk för denna hierarki:
interface IGenericForInferredType<T extends IInferredTypeBase> {
}
Det jag vill är att få TypeScript att automatiskt infera det generiska argumentet i en situation som denna:
type ICallback<TInferredType extends IInferredTypeBase, TGenericForInferredType extends IGenericForInferredType<TInferredType>> = (instance: TInferredType) => any;
function register<
TInferredType extends IInferredTypeBase,
TGenericForInferredType extends IGenericForInferredType<TInferredType>
>(
param1: new () => TGenericForInferredType,
callback: ICallback<TInferredType, TGenericForInferredType>
) {
//...
}
Så, det finns funktionen register som tar 2 argument: param1 är någon slags beskrivning av en typ som är IGenericForInferredType<TInferredType>, bara för att det andra parametern callback ska ha sitt argument som TInferredType infererat. Men det fungerar inte.
Så, låt oss ta ett konkret exempel på detta.
Det finns denna klass för param1:
class ClassImplementingGenericForInferredType implements IGenericForInferredType<IInferredType> {
}
Och här är ett exempel på ett samtal:
register(ClassImplementingGenericForInferredType, (instance) => {})
Typen av instance är dock basklassen IInferredTypeBase och inte IInferredType.
Hur skulle jag kunna få det att sluta sig till IInferredType?
Det betyder att jag inte vill skriva ordet IInferredType någonstans när jag anropar register, jag vill att TypeScript ska lista ut det åt mig.
Motivation: ClassImplementingGenericForInferredType, IInferredType, IInferredTypeBase --> Dessa genereras från C#, deras hierarkiska och generiska relationer definieras i C#, jag vill inte skriva det 2 gånger
Här är en TypeScript Playground-länk som visar problemet, och som en reservlösning, ett GitHub-repo
Svar #1
TypeScripts typesystem är strukturellt, inte nominellt. Så en generisk typ som
interface Gen<T extends Base> {}
där den generiska typparametern T inte förekommer i definitionen (det är bara {}) har inget strukturellt beroende av T. Och därför kan T inte möjligen härledas från den. I ovanstående är Gen<A> och Gen<B> båda bara {}, och att titta på {} kan inte pålitligt ge dig någon information om A och B. Se relevant TS FAQ-post.
Du behöver ett visst strukturellt beroende, som
interface Gen<T extends Base> {
t: T;
}
Igen
type ICallback<T extends Base, G extends Gen<T>> = (instance: T) => any;
har ingen strukturell beroende av G, och därför kan du inte på något sätt härleda G från det. För denna typ antas G inte fylla någon funktion alls. Vi kan ta bort G från den definitionen, och faktiskt ta bort ICallback helt eftersom indirektionen inte tillför något; vi kan helt enkelt använda (t: T) => any.
Därifrån behöver du se till att du har så få generiska typparametrar som möjligt. Den minsta koden som behövs för att få ditt exempel att fungera ser ut som
function register<T extends Base>(
param1: new () => Gen<T>,
callback: (t: T) => any
) { }
interface Ext extends Base {
myProperty: string;
}
class ClassImplementingGenericForInferredType implements Gen<Ext> {
declare t: Ext
}
register(ClassImplementingGenericForInferredType, (instance) => {
instance.myProperty = "...";
})
Här tillåter anropet register() med ClassImplementingGenericForInferredType TypeScript att härleda att T är Ext. Observera att implements-klausulen i klassdeklarationen inte får den här härledningen att lyckas. Sådana klausuler har ingen som helst effekt på härledning, och används enbart för typkontroll av klassens kropp i efterhand. (Du kan ta bort implements-klausulen och se att det inte ändrar något.) Det är t-egenskapen av typen Ext som låter TypeScript härleda T. (Om du försöker ta bort t-egenskapen kommer du att se hur allt exploderar.) Återigen, strukturell typning är grundläggande för TypeScript; deklarationsplatser räknas inte så mycket.
Och när T har härletts som Ext, är den kontextuella typen för callback-parametern instance Ext, och därför finns instance.myProperty.
Playground-länk till kod
