Typescript:抽象泛型类的子类类型

A-S*_*A-S 7 javascript abstract-class typescript

我有一个基本泛型类:

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

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

  internalLogic() {}
}
Run Code Online (Sandbox Code Playgroud)

和继承人:

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";
  }
}
Run Code Online (Sandbox Code Playgroud)

现在我想要定义一个以继承者作为其值的对象:

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,
}
Run Code Online (Sandbox Code Playgroud)

最后,我希望能够使用子级的静态方法,并且还能够创建它们的实例:

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)
*/

Run Code Online (Sandbox Code Playgroud)

jca*_*alz 12

首先,类型

type IChildrenObj = {
  [key: string]: InstanceType<typeof BaseClass>; // instances?
};
Run Code Online (Sandbox Code Playgroud)

不适合描述您的Children对象。 Children存储类构造函数,而InstanceType<typeof BaseClass>即使它适用于抽象类(正如您所指出的,它不起作用),也会谈论类实例。这样写会更接近

type IChildrenObj = {
  [key: string]: typeof BaseClass; // more like constructors
};
Run Code Online (Sandbox Code Playgroud)

但这也不是Children商店的内容:

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.
}
Run Code Online (Sandbox Code Playgroud)

该类型typeof BaseClass有一个抽象构造签名,看起来像new <T>() => BaseClass<T>;调用者(或更有用的是扩展的子类BaseClass)可以选择他们想要的任何内容T,并且BaseClass必须能够处理它。但是类型typeof Child1typeof Child2无法BaseClass<T>T调用者new Child1()或扩展者class Grandchild2 extends Child2想要的任何类型生成; Child1只能构造 aBaseClass<Item1>并且Child2只能构造 a BaseClass<Item2>

所以目前IChildrenObj说它拥有构造函数,每个构造函数都可BaseClass<T>以为每种可能的类型生成一个T。实际上,您想要说的是它拥有构造函数,每个构造函数都可以为某种可能的类型IChildrenObj生成 a 。“每个”和“某些”之间的差异与类型参数的量化方式之间的差异有关;TypeScript(以及大多数其他具有泛型的语言)仅直接支持“every”或通用量化。不幸的是,没有对“某些”或存在量化的直接支持。有关开放功能请求,请参阅microsoft/TypeScript#14446 。BaseClass<T>TT

有一些方法可以在 TypeScript 中准确地编码存在类型,但除非您真的关心类型安全,否则这些方法可能有点太烦人了。(但如果需要的话我可以详细说明)

相反,我在这里的建议可能是重视生产力而不是完整的类型安全性,并且只使用故意宽松的any类型来表示T您不关心的类型。


因此,这是定义的一种方法IChildrenObj

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
}
Run Code Online (Sandbox Code Playgroud)

类型SubclassOfBaseClass是以下各项的交集:生成实例的具体构造签名BaseClass<any>;以及一个映射类型,它获取所有静态成员,但typeof BaseClass同时也获取有问题的抽象构造签名。

让我们确保它有效:

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"]
Run Code Online (Sandbox Code Playgroud)

看起来不错。


这里需要注意的是,虽然IChildrenObj会起作用,但它的类型太模糊,无法跟踪您可能关心的事情,例如 的特定键/值对Children,尤其是索引签名anyBaseClass<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
Run Code Online (Sandbox Code Playgroud)

因此,在这种情况下,我的建议是仅确保可Children分配给IChildrenObj而不实际对其进行注释。例如,您可以使用辅助函数:

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

const Children = asChildrenObj({
  C1: Child1,
  C2: Child2,
}); // okay
Run Code Online (Sandbox Code Playgroud)

现在Children仍然可以在任何需要的地方使用IChildrenObj,但它仍然会记住所有特定的键/值映射,因此当您做坏事时会发出错误:

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'
Run Code Online (Sandbox Code Playgroud)

IChildrenObj如果您需要,您仍然可以使用:

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

Playground 代码链接