ärvning - TypeScript, generiska typer och instanceof
Jag har följande kod:
abstract class BaseToken {}
class OpenParen extends BaseToken {
public static assert(t: BaseToken): OpenParen {
if (t instanceof OpenParen) {
return t;
} else {
throw typeAssertionError;
}
}
}
class CloseParen extends BaseToken {
public static assert(t: BaseToken): CloseParen {
if (t instanceof CloseParen) {
return t;
} else {
throw typeAssertionError;
}
}
}
Funktionerna assert kan användas för att kontrollera om en given instans av BaseToken faktiskt är en viss specialisering av den.
const t: BaseToken = getTokenFromSomewhere();
const oparen = OpenParen.assert(t); // type of oparen is OpenParen, exception if not
Eftersom det finns många fler klasser än OpenParen och CloseParen som härstammar från BaseToken, är detta uppenbarligen mycket mallkod. Jag skulle vilja flytta implementeringen av assert till basklassen. Följande fungerar inte:
abstract class BaseToken {
public static assert<T>(t: BaseToken): T {
if (t instanceof T) { // T used as a value here
return t;
} else {
throw typeAssertionError;
}
}
}
class OpenParen extends BaseToken {}
class CloseParen extends BaseToken {}
Problemet är att å ena sidan kompilerar inte detta eftersom instanceof kräver ett värde, inte en klass, och å andra sidan binder inte detta tillvägagångssätt T till subklasstypen, så OpenParen.assert skulle fortfarande vara generisk.
Jag tror att jag skulle kunna uppnå detta i C++ med Curiously recurring template pattern (CRTP), men det fungerar inte i TypeScript. Finns det något annat trick för att göra samma sak?
Svar #1
Inuti en statisk metod bör this referera till den aktuella konstruktören som används, så implementeringen bör bara kontrollera t instanceof this.
Att få typerna att fungera är knepigare. Ett problem är att det inte finns något polymorft this för statiska metoder; se microsoft/TypeScript#5863. Du skulle vilja kunna säga att assert() returnerar ett värde av instanstypen för den konstruktor du anropar den på. För instansmetoder skulle du kunna säga att den returnerar typen som heter this... men det finns ingen motsvarande this-typ för statiska metoder.
Det finns dock lösningar. Här är ett möjligt sätt att göra det:
abstract class BaseToken {
public static assert<T extends BaseToken>(this: new (...args: any) => T, t: BaseToken): T {
if (t instanceof this) {
return t;
} else {
throw new Error("YOU DID A BAD THING");
}
}
}
Här gör vi assert till en generisk metod där typparametern T är avsedd att vara instanstypen av vilken subklass som används. Och vi använder en this-parameter för att tala om för kompilatorn att du bara kommer att kalla assert() på ett konstruktörsobjekt som konstruerar T-instanser.
Låt oss prova det:
class OpenParen extends BaseToken {
someProp = 123;
}
class CloseParen extends BaseToken {
otherProp = "abc";
}
const openP: BaseToken = new OpenParen();
const closeP: BaseToken = new CloseParen();
console.log(OpenParen.assert(openP).someProp.toFixed(2)); // 123.00
console.log(CloseParen.assert(closeP).otherProp.toUpperCase()) // ABC
Både openP och closeP är kända för kompilatorn endast som BaseTokens, men OpenParen.assert(openP) och OpenParen.assert(closedP) returnerar OpenParen respektive CloseParen, och du kan komma åt de subklass-specifika egenskaperna jag lade till för att visa att detta fungerar.
Playground-länk till kod
Svar #2
Redigera
Det finns en lösning som inte kräver en instans. Den har fortfarande inte exakt den syntax du vill ha, men uppnår åtminstone samma sak den här gången.
(Krediter för Constructor-typen till https://www.simonholywell.com/post/typescript-constructor-type.html)
type Constructor<T extends {} = {}> = new (...args: any[]) => T;
function isOfConstructor<T>(cns: Constructor<T>, b: BaseToken): b is T{
return cns.prototype === b.constructor.prototype;
}
function assert<T>(cns: Constructor<T>, b: BaseToken) : T{
if (isOfConstructor(cns, b)){
return b;
}
else {
throw new Error("not of that type");
}
}
// then call it with
assert(OpenParen, getTokenFromSomewhere());
Original "icke-statisk" lösning
Om du inte behöver att assert är statisk kan du klara dig med
abstract class BaseToken{
private hasSameTypeAs<T extends BaseToken>(t: BaseToken): t is T{
return t.constructor.prototype === this.constructor.prototype;
}
public assert<T extends BaseToken>(t: T): T {
if (this.hasSameTypeAs(t)) {
return t;
} else {
throw new Error("foo");
}
}
}
