[英]How to setup typescript generics in class constructors and functions
我有以下基本接口( Animal
)和兩個實現( Dog
和Cat
),其中每個實現都有自己的屬性( DogProps
和CatProps
)。
interface Animal<T> {
props: T;
eat(): void;
}
interface DogProps {
color: string;
breed: string;
}
class Dog implements Animal<DogProps> {
constructor(public readonly props: DogProps) {}
eat() {}
}
interface CatProps {
color: string;
lives: number;
}
class Cat implements Animal<CatProps> {
constructor(public readonly props: CatProps) {}
eat() {}
}
接下來,我有一個模擬器 class可以接收可選的初始動物(或設置默認動物是Dog
)。 用戶還可以隨時使用模擬器將動物更改為其他任何東西(例如Cat
)。 接口Animal<T>
是公共的,因此用戶可以定義自己的新實現,例如Bird<BirdProps>
並通過模擬器運行它。
我對如何定義模擬器以能夠在不了解T
(屬性)的情況下采用Animal<T>
的任何實現有疑問。 我嘗試了這兩個,但它不起作用:
interface SimulatorSettings<T> {
initialAnimal: Animal<T>;
simulationSteps: number;
}
class SimulatorTry1<T> {
private _animal: Animal<T>;
constructor(settings?: Partial<SimulatorSettings<T>>) {
// Issue 1: Type 'Dog | Animal<T>' is not assignable to type 'Animal<T>'
this._animal = settings?.initialAnimal ?? new Dog({ color: 'white', breed: 'samoyed' });
this.doStuff(settings?.simulationSteps ?? 100);
}
get animal(): Animal<T> {
return this._animal;
}
setAnimal(animal: Animal<T>, settings: Omit<SimulatorSettings<T>, 'initialAnimal'>) {
this._animal = animal;
this.doStuff(settings.simulationSteps);
}
private doStuff(steps: number) {}
}
class SimulatorTry2 {
// Issue 1: Unable to set <T> here because T is undefined
private _animal: Animal<any>;
// Issue 2: Unable to set "constructor<T>": Type parameters cannot appear on a constructor declaration.
constructor(settings?: Partial<SimulatorSettings<T>>) {
this._animal = settings?.initialAnimal ?? new Dog({ color: 'white', breed: 'samoyed' });
this.doStuff(settings?.simulationSteps ?? 100);
}
// Issue3: Unable to set "get animal<T>": An accessor cannot have type parameters.
get animal(): Animal<T> {
return this._animal;
}
setAnimal<T>(animal: Animal<T>, settings: Omit<SimulatorSettings<T>, 'initialAnimal'>) {
this._animal = animal;
this.doStuff(settings.simulationSteps);
}
private doStuff(steps: number) {}
}
這是帶有完整代碼的Typescript Playground的鏈接。
我的問題是:這是否可能(我認為是)以及如何在不定義T = DogProps | CatProps
的情況下做到這一點 T = DogProps | CatProps
因為用戶可以創建應該支持的新實現?
interface BirdProps {
wingSpan: number;
}
class Bird implements Animal<BirdProps> {
constructor(public readonly props: BirdProps) {}
eat() {}
}
const simulator = new SimulatorTry1();
// Color exists because the default animal is dog
const color = simulator.animal.props.color;
// Now the animal is bird, so props are BirdProps
simulator.setAnimal(new Bird({ wingSpan: 20 }), { simulationSteps: 10 });
const span = simulator.animal.props.wingSpan;
不幸的是,這是不可能的; TypeScript 沒有辦法表示可變類型,其中一些操作產生一個類型的值,然后相同的操作產生一個任意不同類型的值。 它可以通過控制流分析獲得更多關於它的信息來 縮小值的明顯類型,但你並不僅僅試圖縮小范圍。 大概您希望看到這種情況發生:
const simulator = new SimulatorTry1();
const c0 = simulator.animal.props.color; // string
simulator.setAnimal(new Bird({ wingSpan: 20 }), { simulationSteps: 10 });
const c1 = simulator.animal.props.color // undefined, or possibly compiler error
但是如果c0
是string
類型,那么c1
確實必須是可分配給string
的類型。 它不能真的是undefined
。
控制流分析有時會重置明顯的類型並因此重新擴展它,因此您可以想象創建一個unknown
類型,然后進行一系列縮小和重置。 但是這些重置只發生在像simulator.animal.props.color = undefined
這樣的顯式分配時。 您不能通過調用simulator.setAnimal()
來實現這一點。 為了使simulator.setAnimal()
改變simulator.animal.props
的明顯類型,它必須是一個斷言方法......而且這些只是狹窄的。
所以我們被困住了; 這是不可能的。 microsoft/TypeScript#41339有一個建議來支持可變類型。 它目前被標記為“等待更多反饋”,這意味着他們想在考慮實施它之前從社區聽到令人信服的用例。 如果您認為這個Simulator
用例很重要,您可以在那里 go 並描述它以及為什么可用的解決方法不可接受。 但我不知道這會有多大幫助。
我可以在這里想象的解決方法是用不變性替換有狀態,至少在類型級別。 也就是說,對setAnimal()
的調用應該創建一個新的模擬器,或者它至少應該看起來像它。 例如,這是一種使用斷言方法的方法,其中調用setAnimal()
實質上會使當前模擬器無效,並且您需要訪問其simulator
屬性以獲取“新”模擬器,即使在運行時確實只有一個:
class Simulator<T = DogProps> {
private _animal: Animal<T>;
constructor(settings?: Partial<SimulatorSettings<T>>);
constructor(settings: SimulatorSettings<T>) {
this._animal = settings?.initialAnimal ?? new Dog({ color: 'white', breed: 'samoyed' });
this.doStuff(settings?.simulationSteps ?? 100);
}
get animal(): Animal<T> {
return this._animal;
}
setAnimal<U>(animal: Animal<U>,
settings: Omit<SimulatorSettings<U>, 'initialAnimal'>
): asserts this is {
animal: never;
setAnimal: never;
simulator: Simulator<U>;
};
setAnimal(animal: Animal<any>, settings: SimulatorSettings<any>) {
this._animal = animal;
this.doStuff(settings.simulationSteps);
this.simulator = this;
}
simulator: unknown;
private doStuff(steps: number) { }
}
然后這就是你使用它的方式:
const sim1: Simulator = new Simulator();
const color = sim1.animal.props.color.toUpperCase();
console.log(color) // WHITE
sim1.setAnimal(new Bird({ wingSpan: 20 }), { simulationSteps: 10 });
// now you have to abandon sim1
const sim2 = sim1.simulator;
// const sim2: Simulator<BirdProps>
const span = sim2.animal.props.wingSpan.toFixed(2);
console.log(span) // "20.00"
或者你可以只生成新的模擬器,所以沒有失效:
class Simulator<T = DogProps> {
private _animal: Animal<T>;
constructor(settings?: Partial<SimulatorSettings<T>>);
constructor(settings: SimulatorSettings<T>) {
this._animal = settings?.initialAnimal ?? new Dog({ color: 'white', breed: 'samoyed' });
this.doStuff(settings?.simulationSteps ?? 100);
}
get animal(): Animal<T> {
return this._animal;
}
spawnSimulator<U>(animal: Animal<U>,
settings: Omit<SimulatorSettings<U>, 'initialAnimal'>): Simulator<U> {
return new Simulator({ initialAnimal: animal, ...settings });
}
private doStuff(steps: number) { }
}
這就是你如何使用它:
const sim1 = new Simulator();
// const sim1: Simulator<DogProps>
const color = sim1.animal.props.color.toUpperCase();
console.log(color) // WHITE
const sim2 = sim1.spawnSimulator(new Bird({ wingSpan: 20 }), { simulationSteps: 10 });
// const sim2: Simulator<BirdProps>
const span = sim2.animal.props.wingSpan.toFixed(2);
console.log(span) // "20.00"
// you can still access sim1
console.log(sim1.animal.props.breed.toUpperCase()) // "SAMOYED"
無論哪種情況,您都放棄了不受支持的可變類型的想法,而是使用 TypeScript 的普通不可變類型。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.