Använd TypeScript för att dynamiskt sätta en klassens egenskap i konstruktören
Jag försöker skapa en TypeScript-klass MyClass med instansvariabler som sätts dynamiskt i konstruktorn:
const myInstance = new MyClass(({
someField: 'foo'
}))
myInstance.someField // typescript should show this as type string
Hur kan jag använda TypeScript för att skapa MyClass för att visa someField som en strängegenskap hos myInstance?
Kan jag använda generiska typer? Är det överhuvudtaget möjligt?
Svar #1
Konceptuellt vill du säga något som class MyClass<T extends object> extends T eller class MyClass<T extends object> implements T, men TypeScript tillåter inte att en class-instans eller ett interface har dynamiska nycklar. Alla nycklar i en class- eller interface-deklaration måste vara statiskt kända. Så om du vill ha detta beteende behöver du göra något annat än en class-deklaration.
Istället kan du beskriva typen av dina önskade MyClass<T>-instanser samt typen av MyClass-klassens konstruktor, och sedan använda en typförsäkran för att tala om för kompilatorn att ditt faktiska konstruktorsobjekt är av den typen.
Låt oss föreställa oss att dina avsedda MyClass<T>-klassinstanser har alla egenskaper hos T, plus en metod som heter someMethodIGuess(). Då kan din MyClass<T>-typ definieras på detta sätt:
type MyClass<T extends object> = T & {
someMethodIGuess(): void;
}
Du vill att din MyClass-klasskonstruktör ska ha en konstruktion-signatur som tar ett argument av typen T för någon generisk T-objekttyp och producerar en instans av MyClass<T>. Det kan definieras så här:
type MyClassConstructor = {
new <T extends object>(arg: T): MyClass<T>
}
För att implementera denna klass kan vi använda [Object.assign()] för att kopiera konstruktörens argumentegenskaper till instansen, plus alla metoder eller andra saker vi behöver.
const MyClass = class MyClass {
constructor(arg: any) {
Object.assign(this, arg);
}
someMethodIGuess() {
}
}
Men naturligtvis, om vi lämnar det så här, kommer kompilatorn inte att se MyClass som en MyClassConstructor (trots allt kan den inte ha dynamiska nycklar, och jag försökte inte ens berätta för den om T här). Vi behöver en typförklaring för att tala om det för den, så här:
const MyClass = class MyClass {
constructor(arg: any) {
Object.assign(this, arg);
}
someMethodIGuess() {
}
} as MyClassConstructor;
Det kompilerar utan fel. Observera återigen att kompilatorn inte kan förstå att MyClass-implementationen överensstämmer med MyClassConstructor-typen. Genom att använda en typpåstående har jag flyttat ansvaret för att säkerställa att det är korrekt implementerat från kompilatorn (som inte kan göra det) till mig (eller dig om du använder den här koden). Så vi bör vara mycket försiktiga med att kontrollera vad vi har gjort och att vi inte har skrivit fel sak (t.ex. skulle Object.assign(arg, this); istället för Object.assign(this, arg); vara ett problem).
Låt oss prova det:
const myInstance = new MyClass(({
someField: 'foo'
}))
console.log(myInstance.someField.toUpperCase()); // FOO
myInstance.someMethodIGuess(); // okay
Ser bra ut! Kompilatorn förväntar sig att myInstance har en someField-egenskap av typen string, samt att someMethodIGuess()-metoden finns.
Så det är allt. Observera att det finns varningar kring användningen av MyClass på samma sätt som du skulle använda en annan klasskonstruktor. Till exempel kommer kompilatorn aldrig att vara nöjd med en class-deklaration som har dynamiska nycklar, så om du försöker göra en generisk subklass av MyClass med en class-deklaration kommer du att få ett fel:
class SubClass<T extends object> extends MyClass<T> { // error
anotherMethod() { }
}
Om du behöver den typen av saker kommer du att upptäcka att du måste använda samma trick som tidigare med underklassen:
type SubClass<T extends object> = MyClass<T> & { anotherMethod(): void };
type SubClassConstructor = { new <T extends object>(arg: T): SubClass<T> };
const SubClass = class SubClass extends MyClass<any> {
anotherMethod() { }
} as SubClassConstructor;
Playground-länk till kod
Svar #2
Vad sägs om detta?
class MyClass{
constructor(props: any) {
Object.assign(this, props);
}
baz(): string {
return "Hello World";
}
static create<T>(props = {} as T) {
return new MyClass(props || {}) as MyClass & T
}
}
const o = MyClass.create({
foo: 1,
bar: "Hello World"
});
console.log(o.foo);
console.log(o.bar);
console.log(o.baz());
TS lekplats
