javascript - Typescript: type på subklasser av en abstrakt generisk klasse


Jeg har en basegenerisk klasse:

abstract class BaseClass<T> {
  abstract itemArray: Array<T>;

  static getName(): string {
    throw new Error(`BaseClass - 'getName' was not overridden!`);
  }

  internalLogic() {}
}

og arvtagere:

type Item1 = {
  name: string
}
class Child1 extends BaseClass<Item1> {
  itemArray: Array<Item1> = [];
  static getName(): string {
    return "Child1";
  }
}


type Item2 = {
  name: number
}
class Child2 extends BaseClass<Item2> {
  itemArray: Array<Item2> = [];
  static getName(): string {
    return "Child2";
  }
}

Nå vil jeg måtte definere et objekt med arvingene som dets verdier:

type IChildrenObj = {
  [key: string]: InstanceType<typeof BaseClass>;
};

/* 
The following error is received: Type 'typeof BaseClass' does not satisfy the constraint 'new (...args: any) => any'.
  Cannot assign an abstract constructor type to a non-abstract constructor type. ts(2344)
*/

const Children: IChildrenObj = {
  C1: Child1,
  C2: Child2,
}

Til slutt vil jeg kunne bruke statiske metoder til barna, og også kunne opprette instanser av dem:

const child: typeof BaseClass = Children.C1;
/*
received the following error: Property 'prototype' is missing in type '{ getName: () => string; }' but required in type 'typeof BaseClass'. ts(2741)
*/

console.log(child.getName());
const childInstance: BaseClass = new child();
/*
The following 2 errors are received:
(1) Generic type 'BaseClass<T>' requires 1 type argument(s). ts(2314)
(2) Cannot create an instance of an abstract class. ts(2511)
Generic type 'BaseClass<T>' requires 1 type argument(s). ts(2314)
*/

Svar #1

For det første, typen

type IChildrenObj = {
  [key: string]: InstanceType<typeof BaseClass>; // instances?
};

er ikke passende for å beskrive ditt Children-objekt. Children lagrer klassekonstruktører mens InstanceType<typeof BaseClass>, selv om det fungerte for abstrakte klasser (som, som du bemerket, det ikke gjør), ville snakke om klasseinstanser. Det ville være nærmere å skrive

type IChildrenObj = {
  [key: string]: typeof BaseClass; // more like constructors
};

Men det er heller ikke det Children lagrer:

const Children: IChildrenObj = {
  C1: Child1, // error!
  // Type 'typeof Child1' is not assignable to type 'typeof BaseClass'.
  // Construct signature return types 'Child1' and 'BaseClass<T>' are incompatible.
  C2: Child2, // error!
  // Type 'typeof Child2' is not assignable to type 'typeof BaseClass'.
  // Construct signature return types 'Child2' and 'BaseClass<T>' are incompatible.
}

Typen typeof BaseClass har en abstrakt konstruktørsignatur som ser omtrent slik ut: new <T>() => BaseClass<T>; kallere (eller mer nyttig, underklassene som utvider BaseClass) kan velge hva som helst for T, og BaseClass må kunne håndtere det. Men typene typeof Child1 og typeof Child2 kan ikke produsere BaseClass<T> for noen T som kalleren av new Child1() eller utvidelsen class Grandchild2 extends Child2 ønsker; Child1 kan kun konstruere en BaseClass<Item1> og Child2 kan kun konstruere en BaseClass<Item2>.

Så for øyeblikket sier IChildrenObj at det holder konstruktører som hver kan produsere en BaseClass<T> for hver mulig type T. Det du egentlig ønsker er at IChildrenObj sier at det holder konstruktører som hver kan produsere en BaseClass<T> for noen mulig type T. Forskjellen mellom "hver" og "noen" har å gjøre med forskjellen i hvordan typeparameteren T kvantifiseres; TypeScript (og de fleste andre språk med generics) støtter bare direkte "hver", eller universell kvantifisering. Dessverre finnes det ingen direkte støtte for "noen", eller eksistensiell kvantifisering. Se microsoft/TypeScript#14446 for den åpne funksjonsforespørselen.

Det finnes måter å nøyaktig enkode eksistensielle typer i TypeScript, men disse er sannsynligvis litt for irriterende å bruke med mindre du virkelig bryr deg om typesikkerhet. (Men jeg kan utdype hvis dette er nødvendig)

I stedet er mitt forslag her sannsynligvis å verdsette produktivitet over full typetilhørighet og bare bruke den bevisst løse any-typen for å representere T du ikke bryr deg om.


Så, her er en måte å definere IChildrenObj på:

type SubclassOfBaseClass =
  (new () => BaseClass<any>) & // a concrete constructor of BaseClass<any>
  { [K in keyof typeof BaseClass]: typeof BaseClass[K] } // the statics without the abstract ctor

/* type SubclassOfBaseClass = (new () => BaseClass<any>) & {
    prototype: BaseClass<any>;
    getName: () => string;
} */

type IChildrenObj = {
  [key: string]: SubclassofBaseClass
}

Typen SubclassOfBaseClass er skjæringspunktet mellom: en konkret konstruksjonssignatur som produserer BaseClass<any>-instanser; og en kartlagt type som henter alle de statiske medlemmene fra typeof BaseClass uten å også hente den problematiske abstrakte konstruksjonssignaturen.

La oss sørge for at det fungerer:

const Children: IChildrenObj = {
  C1: Child1,
  C2: Child2,
} // okay

const nums = Object.values(Children)
  .map(ctor => new ctor().itemArray.length); // number[]
console.log(nums); // [0, 0]

const names = Object.values(Children)
  .map(ctor => ctor.getName()) // string[]
console.log(names); // ["Child1", "Child2"]

Ser bra ut.


Forbeholdet her er at, mens IChildrenObj vil fungere, er det en for vag type til å holde styr på ting du kanskje bryr deg om, som de spesifikke nøkkel-/verdiparene til Children, og spesielt den merkelige «alt går»-oppførselen til indeks-signaturer og any i BaseClass<any>:

// index signatures pretend every key exists:
try {
  new Children.C4Explosives() // compiles okay, but
} catch (err) {
  console.log(err); // 💥 RUNTIME: Children.C4Explosives is not a constructor
}

// BaseClass<any> means you no longer care about what T is:
new Children.C1().itemArray.push("Hey, this isn't an Item1") // no error anywhere

Så mitt forslag i tilfeller som dette er å bare sørge for at Children kan tildeles IChildrenObj uten faktisk å annotere det som sådan. For eksempel kan du bruke en hjelpefunksjon:

const asChildrenObj = <T extends IChildrenObj>(t: T) => t;

const Children = asChildrenObj({
  C1: Child1,
  C2: Child2,
}); // okay

Nå kan Children fortsatt brukes hvor som helst du trenger en IChildrenObj, men den husker fortsatt alle de spesifikke nøkkel-/verdikartleggingene, og gir derfor feil når du gjør dumme ting:

new Children.C4Explosives() // compiler error!
//Property 'C4Explosives' does not exist on type '{ C1: typeof Child1; C2: typeof Child2; }'

new Children.C1().itemArray.push("Hey, this isn't an Item1") // compiler error!
// Argument of type 'string' is not assignable to parameter of type 'Item1'

Du kan fortsatt bruke IChildrenObj hvis du trenger det:

const anotherCopy: IChildrenObj = {};
(Object.keys(Children) as Array<keyof typeof Children>)
  .forEach(k => anotherCopy[k] = Children[k]);

Lenke til kode på lekeplassen

Relaterte artikler