簡體   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