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