typescript - Typ som representerar en generisk klass med en privat konstruktor
Jag har en enkel funktion som hävdar att ett objekt är en instans av en klass. Objektet och klassen skickas båda som argument till funktionen.
Följande är ett förenklat exempel som illustrerar problemet (utan att gå in på detaljerna i den verkliga koden):
function verifyType<T>(instance: unknown, classType:new () => T):instance is T {
if (!(instance instanceof classType)) throw(`Expecting instance of ${classType.name}`);
return true;
}
(TypeScript Playground-länk)
Denna funktion fungerar bra med de flesta klasser:
class Foo {
constructor() {
}
}
const foo:unknown = new Foo();
verifyType(foo, Foo);
// OK
Men jag får kompilatorfel om klassen har en privat konstruktor.
Jag förstår anledningarna till detta och det är logiskt. En konstruktorstyp innebär att den anropande koden kan konstruera klassen och en privat konstruktor betyder att detta inte är tillåtet.
Jag kan dock inte hitta ett alternativ som fortfarande tillåter mig att använda instanceof:
class Bar {
private constructor() {
}
static create() {
return new Bar();
}
}
const bar:unknown = Bar.create();
verifyType(bar, Foo);
// OK
verifyType(bar, Bar);
// Argument of type 'typeof Bar' is not assignable to parameter of type 'new () => Bar'.
// Cannot assign a 'private' constructor type to a 'public' constructor type.
Försöker T extends typeof Object
Jag läste Hur man refererar till en klass med privat konstruktor i en funktion med generiska typparametrar?
Baserat på svaret på den frågan, tänkte jag att jag kanske kunde använda följande:
function verifyType<T extends typeof Object>(instance: unknown, classType:T):instance is InstanceType<T>
Detta verkar dock orsaka fel på grund av Objects många statiska metoder:
const foo = new Foo();
verifyType(foo, Foo);
// Argument of type 'typeof Foo' is not assignable to parameter of type 'ObjectConstructor'.
// Type 'typeof Foo' is missing the following properties from type 'ObjectConstructor': getPrototypeOf, getOwnPropertyDescriptor, getOwnPropertyNames, create, and 16 more.
Att bli exotisk
Jag provade många varianter på typeof Object för att se om jag både kunde tillfredsställa TypeScript och även säkerställa korrekt körningstid, inklusive:
function verifyType<T extends Function & Pick<typeof Object, "prototype">>(instance: unknown, classType:T):instance is InstanceType<T>
Men även om detta löste kompileringstidsfelen, verkar det ha gjort det genom att tillåta typer som misslyckas vid körning, vilket inte är acceptabelt:
verifyType(bar, ()=>{});
// No compile-time errors
// Runtime Error: Function has non-object prototype 'undefined' in instanceof check
Hjälp mig // @ts-expect-error, du är mitt enda hopp
I slutändan kan detta vara ett alltför nischat användningsfall och jag kanske måste acceptera att detta kantfall inte kommer att stödjas inom den närmaste framtiden och undanta min kod därefter.
// @ts-expect-error Unfortunately, TS has no type representing a generic class with a private constructor
verifyType(bar, Bar);
Svar #1
Mitt föreslagna tillvägagångssätt är att göra inferens på prototype-egenskapen hos classType-parametern, på detta sätt:
function verifyType<T extends object>(
instance: unknown,
classType: Function & { prototype: T }
): asserts instance is T {
if (!classType.prototype) throw (
`Wait, ${classType.name || "that"} is not a class constructor`);
if (!(instance instanceof classType)) throw (
`Expecting instance of ${classType.name}`);
}
Generellt sett låter TypeScript dig skriva x instanceof y om y är av någon funktionstyp, alltså Function &, och sedan härleder vi den generiska typparametern T som motsvarar instanstypen av classType genom att säga att det är typen av prototype-egenskapen (TypeScript modellerar klasskonstruktörers prototyper som att de är samma typ som en instans av klassen, även om det i praktiken oftast inte stämmer. Se microsoft/TypeScript#44181 och troligen andra för mer information).
Utöver Function & {protoype: T} har jag gjort några ändringar från din kod:
verifyType()är nu en assertionsfunktion som returnerarasserts instance is Tistället för en typvaktfunktion som returnerarinstance is T. Typvaktfunktioner ska returneratrueellerfalseoch kompilatorn använder detta i sin kontrollflödesanalys. Assertionsfunktioner å andra sidan returnerar inget värde, de säkerställer bara att det som kan begränsas verkligen är begränsat. Om du har en typvaktfunktion som alltid returnerartrue(och annars kastar ett undantag) vill du troligen istället ha en assertionsfunktion.Jag har gjort en explicit kontroll inuti kroppen för att se om
classType.prototypeär definierad. TypeScript gerFunctionenprototype-egenskap av typenanyistället för den säkrare typenunknown, och detta gör tyvärr nästan omöjligt att upptäcka att en icke-konstruktor som()=>{}skulle kasta ett runtime-fel om den gjordes till målet förinstanceof. Istället för att försöka representera det i typsystemet, försöker jag därför bara stärkaverifyType()s implementation mot sådana kantfall. Du kanske vill kontrollera(!classType.prototype || typeof classType.prototype !== "object")eller någon annan logik som du anser nödvändig.
Låt oss testa det:
class Foo {
constructor() {
}
a = 1;
}
const foo: unknown = new Foo();
foo.a // error! Object is of type unknown
verifyType(foo, Foo);
foo.a // okay
console.log(foo.a.toFixed(1)) // "1.0"
Ser bra ut, kompilatorn ser att foo är en Foo efter (men inte innan) verifyType(foo, Foo) har kallats.
class Bar {
private constructor() {
}
static create() {
return new Bar();
}
b = "z"
}
const bar: unknown = Bar.create();
verifyType(bar, Bar);
console.log(bar.b.toUpperCase()); // "Z"
Ser också bra ut. Kompilatorn tillåter gärna verifyType(bar, Bar) eftersom Bar har en prototype-egenskap av typen Bar (eller åtminstone verkar det så för kompilatorn) och därför kan du sedan komma åt b-egenskapen hos Bar. Det spelar ingen roll att Bar har en private-konstruktor.
Och slutligen:
const baz: unknown = new Date();
verifyType(baz, () => { }); // 💥 Wait, that is not a class constructor
baz // any
Vi kunde inte markera verifyType(baz,()=>{}) vid kompilering, men åtminstone får du ett meningsfullt fel vid körning, så den efterföljande ”avsmalningen” av baz från unknown till any (usch, tack any) påverkar oss inte.
Playground-länk till kod
Svar #2
Som tur är finns det en enkel lösning
Jag stötte på samma problem för en tid sedan. Du använder i praktiken den inbyggda InstanceType<TClass> som bestämmer instanstypen men kommer med kravet på en publik konstruktor:
type InstanceType<T extends abstract new (...args: any) => any> = ....Dock är kravet på InstanceType<TClass> att ha en offentlig konstruktor väldigt begränsande; Det är okej om vi bara behöver resultat vid körning, men värdelöst om vi vill skriva öppna generiska typer (TS Conditional Types har en irriterande tendens att mixa och matcha resultat från körningstyper med resultat från kompileringstyper hejvilt!)
Som tur är kan vi utnyttja fullt ut hur kompilatorn löser typer till den bäst matchande signaturen och ignorera åtkomstmodifikatorn för att acceptera privata/skyddade konstruktorer på följande sätt:
type InstanceTypeSpy<TClass> = InstanceType<{ new(): never } & TClass>;Det fungerar eftersom tekniskt sett new(): never verkligen uppfyller förväntningarna på InstanceType<>, men kompilatorn (som utvärderar och kan se alla signaturmatchningsmöjligheter oavsett åtkomstmodifierare) väljer att inte matcha mot new(): never för att undvika never om möjligt - vilket ger oss den verkliga typen (eller återgår till never om det verkligen inte finns någon konstruktor på TClass).
Kuriosa
För att förklara vad jag menar med körningstid kontra kompileringstidstyper, överväg den nyligen tillagda Awaited<T> - den ger oss körningstidstypen av att vänta på en instans av T. Tänk nu på att vi skriver ett generiskt bibliotek som behöver hantera async/T=Promise<U> så att den exakta typen T inte är känd vid bibliotekets kompileringstid (eftersom U är okänd).
Det skulle vara en "öppen" generisk typ - När U är känd blir det en "stängd" generisk typ.
Till exempel Map<K,V> är en öppen typ, och Map<string,object> är en stängd typ.
I sådana fall behöver vi kompileringstidstypen; dvs. om T=Promise<U> så behöver vi U.
Vi kan inte lösa det med Awaited<T> eftersom det kanske inte är lika med U. Om du skriver några enhetstester och försöker tilldela ett värde av typen U till något som förväntar sig Awaited<T> kommer du att få ett kompilatorfel som säger att det inte går att tilldela.
För att förstå varför, kom ihåg att await faktiskt är rekursivt! Fundera på vad som händer om U vid körning är av typen Promise<V>, vilket skulle innebära att vår T är av typen Promise<Promise<V>>. Vid körning kommer väntan på en instans av typen T att ge ett resultat av typen V vilket verkligen är mycket annorlunda än ett resultat av typen Promise<V>. Så för en okänd T kan kompilatorn inte garantera korrektheten för sådan biblioteks kod.
För fullständighetens skull här är Awaited gjord icke-rekursiv:
/**
* Identical to Awaited<T> except it is NOT recursive, thus giving the compile-time type
* (rather than the runtime type) which is what you need when writing generics.
*/
export type PromiseResolveType<T> =
T extends null | undefined ? T : // special case for `null | undefined` when not in `--strictNullChecks` mode
T extends object & { then(onfulfilled: infer F): any } ? // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
F extends ((value: infer V, ...args: any) => any) ? V : // if the argument to `then` is callable, extracts the unwrapped value
never : // the argument to `then` was not callable
T; // non-object or non-thenable
