typeofClass<T>Class<T> 不匹配抽象类本章中,我们将探讨将类作为值
考虑以下类
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}此函数接受一个类并创建它的一个实例
function createPoint(PointClass: ???, x: number, y: number) {
return new PointClass(x, y);
}如果我们希望参数 PointClass 是 Point 或其子类,我们应该使用什么类型?
typeof在 §7.7 “两种语言级别:动态与静态” 中,我们探讨了 TypeScript 的两种语言级别
类 Point 创建了两个东西
PointPoint 实例的接口 Point根据我们提到 Point 的位置,它意味着不同的东西。这就是为什么我们不能对 PointClass 使用类型 Point 的原因:它匹配类 Point 的*实例*,而不是类 Point 本身。
相反,我们需要使用类型运算符 typeof(TypeScript 语法中的另一个部分,它也存在于 JavaScript 中)。typeof v 代表动态(!) 值 v 的类型。
function createPoint(PointClass: typeof Point, x: number, y: number) { // (A)
return new PointClass(x, y);
}
// %inferred-type: Point
const point = createPoint(Point, 3, 6);
assert.ok(point instanceof Point);构造函数类型字面量是一个带有前缀 new 的函数类型字面量(第 A 行)。该前缀表示 PointClass 是一个必须通过 new 调用的函数。
function createPoint(
PointClass: new (x: number, y: number) => Point, // (A)
x: number, y: number
) {
return new PointClass(x, y);
}回想一下,接口和对象字面量类型 (OLT) 的成员 包括方法签名和调用签名。调用签名使接口和 OLT 能够描述函数。
类似地,*构造签名*使接口和 OLT 能够描述构造函数。它们看起来像调用签名,但添加了前缀 new。在下一个示例中,PointClass 具有一个带有构造签名的对象字面量类型
function createPoint(
PointClass: {new (x: number, y: number): Point},
x: number, y: number
) {
return new PointClass(x, y);
}Class<T>凭借我们获得的知识,我们现在可以通过引入类型参数 T 来为作为值的类创建一个泛型类型
type Class<T> = new (...args: any[]) => T;我们也可以使用接口而不是类型别名
interface Class<T> {
new(...args: any[]): T;
}Class<T> 是一种类的类型,其实例与类型 T 匹配。
Class<T> 使我们能够编写 createPoint() 的泛型版本
function createInstance<T>(AnyClass: Class<T>, ...args: any[]): T {
return new AnyClass(...args);
}createInstance() 的使用方法如下
class Person {
constructor(public name: string) {}
}
// %inferred-type: Person
const jane = createInstance(Person, 'Jane');createInstance() 是通过函数实现的 new 运算符。
我们可以使用 Class<T> 来实现类型转换
function cast<T>(AnyClass: Class<T>, obj: any): T {
if (! (obj instanceof AnyClass)) {
throw new Error(`Not an instance of ${AnyClass.name}: ${obj}`)
}
return obj;
}使用 cast(),我们可以将值的类型更改为更具体的类型。这在运行时也是安全的,因为我们既静态地更改了类型,又执行了动态检查。以下代码提供了一个示例
function parseObject(jsonObjectStr: string): Object {
// %inferred-type: any
const parsed = JSON.parse(jsonObjectStr);
return cast(Object, parsed);
}Class<T> 和 cast() 的一个用例是类型安全的 Map
class TypeSafeMap {
#data = new Map<any, any>();
get<T>(key: Class<T>) {
const value = this.#data.get(key);
return cast(key, value);
}
set<T>(key: Class<T>, value: T): this {
cast(key, value); // runtime check
this.#data.set(key, value);
return this;
}
has(key: any) {
return this.#data.has(key);
}
}TypeSafeMap 中每个条目的键都是一个类。该类确定条目值的静态类型,并在运行时用于检查。
这是 TypeSafeMap 的实际应用
const map = new TypeSafeMap();
map.set(RegExp, /abc/);
// %inferred-type: RegExp
const re = map.get(RegExp);
// Static and dynamic error!
assert.throws(
// @ts-expect-error: Argument of type '"abc"' is not assignable
// to parameter of type 'Date'.
() => map.set(Date, 'abc'));Class<T> 不匹配抽象类当需要 Class<T> 时,我们不能使用抽象类
abstract class Shape {
}
class Circle extends Shape {
// ···
}
// @ts-expect-error: Type 'typeof Shape' is not assignable to type
// 'Class<Shape>'.
// Cannot assign an abstract constructor type to a non-abstract
// constructor type. (2322)
const shapeClasses1: Array<Class<Shape>> = [Circle, Shape];为什么?理由是构造函数类型字面量和构造签名应该只用于可以实际 new 调用的值(包含更多信息的 GitHub 问题)。
这是一种解决方法
type Class2<T> = Function & {prototype: T};
const shapeClasses2: Array<Class2<Shape>> = [Circle, Shape];这种方法的缺点
instanceof 检查(作为右侧操作数)。