繁体   English   中英

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

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

我有以下基本接口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() {}
}

接下来,我有一个模拟器 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

但是如果c0string类型,那么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 的普通不可变类型。

Playground 代码链接

暂无
暂无

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

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