类既是值又是类型

2026-09-09 00:00    #typescript  

class 是 TypeScript 里唯一同时占据值空间和类型空间的声明。这解释了为什么 console.log(C.a) 会报错,也解释了 typeof C 和 C 为什么是两回事。

两个身份

 1// 类既是值又是类型
 2
 3class C {
 4  a = "123";                    // 实例属性
 5  static version = "1.0";       // 静态属性
 6
 7  constructor(public n: number) {}
 8
 9  method(): string { return this.a; }
10  static create(): C { return new C(0); }
11}
12
13// ---- 值位置:C 是构造函数 ----
14const ctor = C;                  // ✓ C 是值
15console.log(typeof C);           // "function"
16const instance = new C(1);
17
18// ---- 类型位置:C 是实例类型 ----
19let c: C = new C(2);             // ✓ C 也是类型
20// 注意:作为类型时,C 表示"实例的类型",不含静态成员
21
22// ---- 关键区别:C 与 typeof C ----
23// C(类型位置)= 实例类型:有 a, n, method
24// typeof C      = 构造函数类型:有 version, create, prototype
25
26// 实例类型:能访问实例成员
27const inst: C = new C(3);
28console.log(inst.a, inst.n, inst.method());
29
30// 构造函数类型:能访问静态成员
31const ctorTyped: typeof C = C;
32console.log(ctorTyped.version, ctorTyped.create().n);
33
34// ---- 常见错误:把类当实例用 ----
35// 原归档笔记里的经典问题:
36class D { a = "123" }
37const d: D = new D();
38console.log(d);        // D { a: '123' }
39console.log(D);        // [class D]
40// @ts-expect-error TS2339: a 是实例属性,不在类(构造函数)上
41console.log(D.a);
42
43// ---- 用 typeof C 表达"传入一个类" ----
44function instantiate<T>(Ctor: new (...args: any[]) => T): T {
45  return new Ctor();
46}
47const made = instantiate(C);   // T 推断为 C
48
49// 更精确的构造函数类型
50type Ctor<T> = new (...args: any[]) => T;
51function make<T>(ctor: Ctor<T>): T { return new ctor(); }
52
53// 静态侧 + 实例侧一起约束
54interface WithCreate<T> {
55  create(): T;
56}
57function fromStatic<T, S extends WithCreate<T>>(Static: S): T {
58  return Static.create();
59}
60const fromStaticResult = fromStatic(C);
61
62// ---- 抽象类的类型 ----
63abstract class Base {
64  abstract run(): string;
65  static helper() { return "h"; }
66}
67// typeof Base 包含静态成员
68const baseStatic: typeof Base = Base;
69console.log(baseStatic.helper());
70// @ts-expect-error TS2511: 抽象类不能实例化
71new Base();
72
73// ---- 接口实现类时的类型关系 ----
74interface HasA { a: string }
75// 实例类型满足接口
76const asInterface: HasA = new C(4);
77// 但 typeof C 不满足(构造函数没有 a)
78// @ts-expect-error TS2741: typeof C 缺少 a
79const ctorAsInterface: HasA = C;
80
81console.log(ctor, instance, c, ctorTyped, made, make(C), fromStaticResult, asInterface, ctorAsInterface);
1function
2123 3 123
31.0 0
4D { a: '123' }
1class C {
2  a = "123";                // 实例属性
3  static version = "1.0";   // 静态属性
4  method(): string { }
5  static create(): C { }
6}
7
8const ctor = C;              // ✓ 值位置:C 是构造函数
9let c: C = new C();          // ✓ 类型位置:C 是实例类型
位置C 表示什么有哪些成员
值构造函数(运行时函数)version、create、prototype
类型实例类型a、n、method

C 与 typeof C

这是理解类双身份的关键:

1const inst: C = new C();          // C 作类型 = 实例类型
2const ctorTyped: typeof C = C;    // typeof C = 构造函数类型

typeof C 在类型位置查询 C 这个值的类型,得到构造函数类型——包含静态成员。

1inst.a;            // ✓ 实例成员
2inst.method();     // ✓
3
4ctorTyped.version; // ✓ 静态成员
5ctorTyped.create(); // ✓

经典错误:把类当实例用

1class D { a = "123" }
2
3const d: D = new D();
4console.log(d);     // D { a: '123' }  ← 实例
5console.log(D);     // [class D]      ← 构造函数
6
7console.log(D.a);
8// error TS2339: Property 'a' does not exist on type 'typeof D'.

a 是实例属性,只在 new D() 出来的对象上。D 本身是构造函数,它上面只有静态成员和 prototype。

报错信息里的 typeof D

注意错误说的是 typeof D 而不是 D。因为 D 在值位置被使用时,类型检查器看的是它的值的类型,也就是构造函数类型。看到这个措辞就知道是"静态侧没有这个成员"。

接口只约束实例侧

1interface HasA { a: string }
2
3const asInterface: HasA = new C(4);   // ✓ 实例满足
4
5const ctorAsInterface: HasA = C;
6// error TS2741: Property 'a' is missing in type 'typeof C' but required in type
7// 'HasA'.

implements 也是同一规则——它检查实例侧:

1interface Serializable { serialize(): string }
2
3class Foo implements Serializable {
4  serialize() { return ""; }     // 必须是实例方法
5}

用 typeof C 表达"传入一个类"

这是实际开发中常见的需求:

1// 泛型构造函数类型
2function instantiate<T>(Ctor: new (...args: any[]) => T): T {
3  return new Ctor();
4}
5
6const made = instantiate(C);   // T 推断为 C

更精确的写法:

1type Ctor<T> = new (...args: any[]) => T;
2function make<T>(ctor: Ctor<T>): T { return new ctor(); }

需要静态成员时,用接口约束静态侧:

1interface WithCreate<T> {
2  create(): T;
3}
4
5function fromStatic<T, S extends WithCreate<T>>(Static: S): T {
6  return Static.create();
7}

抽象类

 1abstract class Base {
 2  abstract run(): string;
 3  static helper() { return "h"; }
 4}
 5
 6const baseStatic: typeof Base = Base;   // ✓ 静态侧可用
 7baseStatic.helper();
 8
 9new Base();
10// error TS2511: Cannot create an instance of an abstract class.

typeof Base 正常包含静态成员,但实例化被禁止。

为什么这样设计

类的双身份是为了贴合 JavaScript 的实际语义。class 在运行时就是一个构造函数(一个值),而 TypeScript 额外给它一个类型身份来描述实例形状。

对比其他声明:

声明值空间类型空间
class C✓ 构造函数✓ 实例类型
function f✓ 函数✗(用 typeof f)
interface I✗✓
type T✗✓
enum E✓ 对象✓ 枚举类型
const x✓✗(用 typeof x)

只有 class 和 enum 同时占据两个空间。function 是值,要用 typeof f 才能拿到它的类型。

为什么不能用 instanceof 检查 interface

interface 只存在于类型空间,运行时没有对应值,所以 value instanceof SomeInterface 报 TS2693: 'SomeInterface' only refers to a type, but is being used as a value here.。

class 就没这个问题——它在运行时真实存在。详见 类型擦除。

相关