TypeScript: få typeof generisk konstruktor
I TypeScript har jag definierat en hjälpfunktion som kallas create som tar en konstruktor och konstruktorens typade argument för att skapa en ny instans av en klass:
function create<Ctor extends new (...args: any[]) => any, R extends InstanceType<Ctor>>(
ctor: Ctor,
...args: ConstructorParameters<Ctor>
): R {
return new ctor(...args)
}
Detta fungerar för enkla klasser på följande sätt:
class Bar {
constructor(param: number) { }
}
const bar1 = create<typeof Bar, Bar>(Bar, 1);
// Compile error: Argument of type '"string"' is not assignable to parameter of type 'number'.ts(2345)
const bar2 = create<typeof Bar, Bar>(Bar, 'string');
Men om jag har en generell klass kan jag inte få TypeScript att utföra korrekt typkontroll på create:
class Foo<T> {
constructor(param: T) { }
}
// Ok
const foo1 = create<typeof Foo, Foo<number>>(Foo, 1);
// This should be an error but is not
const foo2 = create<typeof Foo, Foo<number>>(Foo, { not: 'a number' })
Grundorsaken är att signaturen för create i det andra fallet tar en unknown-parameter:
Huvudfråga
Är det möjligt att uttryckligen skriva ut typen för den generiska klasskonstruktören i anropet till create utan att inlinera typen själv och samtidigt bevara signaturen för create?
Försök
Min första tanke var:
create<typeof Foo<number>, Foo<number>>(Foo, { not: 'a number' })
Men det är ogiltig kod.
Hittills är den bästa lösningen jag har hittat att använda en tillfällig klass för att uttryckligen fånga konstruktörstypen:
class Temp extends Foo<number> { }
// This correctly generates an error
create<typeof Temp, Foo<number>>(Foo, { not: 'a number' });
Kan detta göras på ett mer elegant sätt?
Svar #1
Funktionen create() bör förmodligen vara generisk i argumentlistan A till konstruktorn, såväl som för den konstruerade instanstypen R. Detta utnyttjar högre ordningens typinferens som introducerades i TypeScript 3.4.
function create<A extends any[], R>(
ctor: new (...args: A) => R,
...args: A
): R {
return new ctor(...args)
}
Därifrån är frågan hur man specificerar typerna så att du kommer att be om en Foo<number> och få någon sorts fel om din parameter inte är korrekt. Ett sätt att göra detta är att manuellt specificera de generiska parametrarna:
// Specify generics
const fooSpecifiedGood = create<[number], Foo<number>>(Foo, 123); // okay
const fooSpecifiedBad = create<[number], Foo<number>>(Foo, { not: 'a number' }); // error
Det fungerar. Om, som du nämnde, din konstruktörs parameterlista är för lång eller komplicerad för att skriva ut, skulle jag föreslå att du bara annoterar typen av en variabel som du sparar den returnerade instansen till, på det här sättet:
// Just annotate the variable you're writing to
const fooAnnotatedGood: Foo<number> = create(Foo, 123); // okay
const fooAnnotatedBad: Foo<number> = create(Foo, { not: 'a number' }); // error
Nu skulle du möjligen föredra att kunna kalla create<Foo<number>>(Foo, 123) där du manuellt anger R-parametern som Foo<number>, men låter kompilatorn härleda A-parametern. Tyvärr stöder TypeScript för närvarande inte sådan partiell typparameterhärledning. För varje givet generisk funktionsanrop kan du antingen manuellt specificera alla typparametrar, eller låta kompilatorn härleda alla typparametrar, och det är i princip allt (standardvärden för typparametrar komplicerar historien något men det ger dig ändå inte denna möjlighet). Ett sätt att kringgå detta är att använda currying för att ta en enda funktion som <A, R>()=>... och omvandla den till en högre ordningsfunktion som <A>()=><R>()=>.... Att tillämpa denna teknik på create() ger dig detta:
// Or get a curried creator function that lets you use specify the instance type
// while allowing the compiler to infer the compiler args
const curriedCreate = <R>() =>
<A extends any[]>(ctor: new (...args: A) => R, ...args: A) => new ctor(...args);
const fooCurriedGood = curriedCreate<Foo<number>>()(Foo, 123); // okay
const fooCurriedBad = curriedCreate<Foo<number>>()(Foo, { not: 'a number' }); // error
Så de är de huvudsakliga alternativen jag ser. Personligen skulle jag använda fooAnnotated-metoden, eftersom den är analog med const oops: Foo<number>=new Foo({not: 'a number'}). Felet ligger inte i att anropa Foo-konstruktorn; det ligger i att anta att resultatet är en Foo<number>. Hur som helst, hoppas att något av de här hjälper dig. Lycka till!
Playground-länk till kod
