typescript - Cum să deriv proprietăți dintr-un parametru al unei clase generice?


Aș dori să implementez o clasă de tip mixin care aplică toate proprietățile unui argument de intrare asupra sa în constructor. Problema pe care o am este că nu pot găsi o modalitate de a tipiza corespunzător o astfel de clasă.

Ideal, mi-ar plăcea să ating toate cele 3 puncte:

  1. Funcționează — Este pur și simplu, din punct de vedere tehnic, fezabil.
  2. Direct — Transmiterea unui parametru de tip către Food ar face ca acesta să moștenească toate proprietățile (împreună cu clasele (Steak) care extind Food).
  3. Stabil — Nu se bazează pe comportamentul nedefinit al TypeScript.
class Thing<T extends Thing<any>> {
  public declare thing: true;

  constructor(public input: T) {}
}

class Meat extends Thing<Thing<any>> {
  public declare meat: true;
}

class Beef extends Meat {
  public declare beef: true;
}

class Food<T extends Meat> extends Thing<T> {
  public declare food: true;
}

class Steak extends Food<Beef> {
  do() {
    this.thing; // ✔️,       derives from `Food → Thing`
    this.meat;  // ❌, should derive from `Beef → Meat`
    this.beef;  // ❌, should derive from `Beef`
    this.food;  // ✔️,       derives from `Food`
  }
}

Am încercat:

  • Tipuri mapate direct în clasă

    1. Funcționează — Nu
    2. Direct — Nu
    3. Stabil — Nu

    Acestea sunt cele două variante pe care le-am încercat mai întâi. Nicio șansă.

    class Food<T extends Meat> extends Thing<T> {
      [K in keyof T]: T[K]; // ']' expected. ts(1005)
    }
    
    class Food<T extends Meat> extends Thing<T> {
      [K in T]: T[K]; // A mapped type may not declare properties or methods. ts(7061)
    }
    
  • Aliașe de tip

    1. Funcționează — Da
    2. Direct — Nu
    3. Stabil — Nu

    Am venit cu acest tip de alias pentru fuziune. Funcționează, într-un fel, dar are mai multe avertismente:

    1. Trebuie aplicat clasei extinse (Steak) în locul clasei de bază (Food)
    2. Necesită parametrul generic al Food (<T extends Meat>), altfel TypeScript va arunca Base constructors must all have the same return type. ts(2510).
    type Cooked<T extends Meat> = (new (
      ...args: ConstructorParameters<typeof Food>
    ) => Food<T> & T) &
      typeof Food;
    
    class Steak extends (Food as Cooked<Beef>) {
      do() {
        this.thing; // true
        this.meat; // true
        this.beef; // true
        this.food; // true
      }
    }
    

    Teren de joacă

  • Interfață generică care extinde ilegal

    1. Funcționează — Da
    2. Direct — Da
    3. Stabil — Nu

    Acesta este cel mai interesant, deoarece pare să funcționeze 100%. Totuși, necesită o directivă // @ts-expect-error pentru a suprima erorile pe care le produce. Asta chiar nu mi se pare corect.

    // without `// @ts-expect-error`:
    //   Interface 'Food<T>' cannot simultaneously extend types 'Thing<T>' and 'T'.
    //    Named property 'input' of types 'Thing<T>' and 'T' are not identical. ts(2320)
    
    // @ts-expect-error
    interface Food<T extends Meat> extends T {}
    
    class Food<T extends Meat> extends Thing<T> {
      public declare food: true;
    }
    
    class Steak extends Food<Beef> {
      do() {
        this.thing; // true
        this.meat; // true
        this.beef; // true
        this.food; // true
      }
    }
    

    Încercarea de a folosi Omit pentru a elimina proprietățile care se suprapun împiedică funcționarea acestuia complet:

    // An interface can only extend an object type or intersection of object types with statically known members. ts(2312)
    interface Food<T extends Meat> extends Omit<T, keyof Thing> {}
    

    Teren de joacă