简体   繁体   English

如何在 class 构造函数和函数中设置 typescript generics

[英]How to setup typescript generics in class constructors and functions

I have the following base interface ( Animal ) and two implementations ( Dog and Cat ) where each implementation has its own properties ( DogProps and CatProps ).我有以下基本接口Animal )和两个实现DogCat ),其中每个实现都有自己的属性( DogPropsCatProps )。

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() {}
}

Next up, I have a simulator class that can receive an optional initial animal (or set the default one which is a Dog ).接下来,我有一个模拟器 class可以接收可选的初始动物(或设置默认动物是Dog )。 Users can also use the simulator to change the animal to anything else (eg Cat ) at any time.用户还可以随时使用模拟器将动物更改为其他任何东西(例如Cat )。 Interface Animal<T> is public, so the user can define its own new implementation , eg Bird<BirdProps> and run it through the simulator.接口Animal<T>是公共的,因此用户可以定义自己的新实现,例如Bird<BirdProps>并通过模拟器运行它。

I have issues with how to define the simulator to be able to take any implementation of Animal<T> without knowing about T (properties).我对如何定义模拟器以能够在不了解T (属性)的情况下采用Animal<T>的任何实现有疑问。 I tried with these two but it is not working:我尝试了这两个,但它不起作用:

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) {}
}

Here is the link to the Typescript Playground with the full code.这是带有完整代码的Typescript Playground的链接。

My question is: Is this possible to do (I assume it is) and how to do it without defining that T = DogProps | CatProps我的问题是:这是否可能(我认为是)以及如何在不定义T = DogProps | CatProps的情况下做到这一点T = DogProps | CatProps because users can create new implementations which should be supported? 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;

This is unfortunately not possible;不幸的是,这是不可能的; TypeScript doesn't have a way to represent mutable types, where some operation produces a value of one type and then later the same operation produces a value of an arbitrarily different type. TypeScript 没有办法表示可变类型,其中一些操作产生一个类型的值,然后相同的操作产生一个任意不同类型的值。 It can narrow the apparent type of a value by gaining more information about it via control flow analysis , but you're not trying to do only narrowing.它可以通过控制流分析获得更多关于它的信息来 缩小值的明显类型,但你并不仅仅试图缩小范围。 Presumably you'd want to see this happen:大概您希望看到这种情况发生:

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

But if c0 is of type string then c1 really must be of a type assignable to string .但是如果c0string类型,那么c1确实必须是可分配给string的类型。 It can't really be undefined .它不能真的是undefined


Control flow analysis does sometimes reset the apparent type and therefore re-widen it, so you could imagine making a type like unknown and then doing a series of narrowings and resettings.控制流分析有时会重置明显的类型并因此重新扩展它,因此您可以想象创建一个unknown类型,然后进行一系列缩小和重置。 But these resettings only happen upon explicit assignment like simulator.animal.props.color = undefined .但是这些重置只发生在像simulator.animal.props.color = undefined这样的显式分配时。 You can't make this happen via a call to simulator.setAnimal() .您不能通过调用simulator.setAnimal()来实现这一点。 In order to make simulator.setAnimal() change the apparent type of simulator.animal.props , it would have to be an assertion method ... and these only narrow.为了使simulator.setAnimal()改变simulator.animal.props的明显类型,它必须是一个断言方法......而且这些只是狭窄的。

So we're stuck;所以我们被困住了; this isn't possible.这是不可能的。 There is a suggestion at microsoft/TypeScript#41339 to support mutable types. microsoft/TypeScript#41339有一个建议来支持可变类型。 It's currently marked as "Awaiting More Feedback", meaning they want to hear compelling use cases from the community before even thinking of implementing it.它目前被标记为“等待更多反馈”,这意味着他们想在考虑实施它之前从社区听到令人信服的用例。 If you think this Simulator use case is important, you could go there and describe it and why the available workarounds aren't acceptable.如果您认为这个Simulator用例很重要,您可以在那里 go 并描述它以及为什么可用的解决方法不可接受。 But I don't know that it would help much.但我不知道这会有多大帮助。


The workarounds I can imagine here are to replace statefulness with immutability, at least at the type level.我可以在这里想象的解决方法是用不变性替换有状态,至少在类型级别。 That is, the call to setAnimal() should create a new simulator, or it should at least look like it does.也就是说,对setAnimal()的调用应该创建一个的模拟器,或者它至少应该看起来像它。 For example, here's a way using an assertion method, where calling setAnimal() essentially invalidates the current simulator, and you need to access its simulator property to get the "new" one, even though there really is only one at runtime:例如,这是一种使用断言方法的方法,其中调用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) { }
}

And then this is how you'd use it:然后这就是你使用它的方式:

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"

Or you can just spawn new simulators, so there's no invalidation:或者你可以只生成新的模拟器,所以没有失效:

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) { }
}

And this is how you'd use it:这就是你如何使用它:

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"

In either case you're giving up on the idea of unsupported mutable types and instead using TypeScript's normal immutable types.无论哪种情况,您都放弃了不受支持的可变类型的想法,而是使用 TypeScript 的普通不可变类型。

Playground link to code Playground 代码链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM