[英]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.