类在 TypeScript 里有两个身份:运行时它是一个构造函数,编译期它是一个类型。这个双重身份是理解类成员修饰符、implements、装饰器的前提。装饰器则是"在类定义时修改类行为"的元编程能力,它有两条互不兼容的路线,选择哪条决定了你的 tsconfig 和整个技术栈。
类成员与修饰符
1// 类成员与修饰符
2
3class Account {
4 // 实例字段:每个实例一份
5 balance = 0;
6
7 // 只读字段:只能在声明处或构造函数里赋值
8 readonly id: string;
9
10 // 静态字段:挂在类上,不是实例上
11 static totalAccounts = 0;
12
13 // 私有字段(TS 修饰符):只在编译期检查
14 private secret = "s";
15
16 // 私有字段(ES 真私有):运行时也访问不到
17 #realSecret = "r";
18
19 // 受保护:子类可以访问
20 protected owner: string;
21
22 // 可选字段
23 nickname?: string;
24
25 constructor(id: string, owner: string) {
26 this.id = id;
27 this.owner = owner;
28 Account.totalAccounts++;
29 }
30
31 // getter / setter
32 get label(): string {
33 return `${this.id}(${this.owner})`;
34 }
35
36 set label(v: string) {
37 this.nickname = v;
38 }
39
40 // 静态方法
41 static reset(): void {
42 Account.totalAccounts = 0;
43 }
44
45 // 读取私有字段的方法
46 reveal(): string {
47 return `${this.secret}/${this.#realSecret}`;
48 }
49}
50
51// 参数属性:构造函数参数前加修饰符,自动成为字段
52class Point {
53 constructor(
54 public x: number,
55 private y: number,
56 readonly z: number = 0,
57 ) {}
58
59 // y 是 private,只能在类内部读
60 sum(): number {
61 return this.x + this.y + this.z;
62 }
63}
64
65// 用 # 真私有:运行时也拿不到
66class Vault {
67 #value = 1;
68 get(): number { return this.#value; }
69}
70
71const a = new Account("a1", "rainboy");
72a.label = "alias";
73a.balance = 100;
74
75const p = new Point(1, 2);
76
77console.log(a.label, a.balance, a.nickname, a.reveal(), Account.totalAccounts);
78console.log(p.x, p.sum(), new Vault().get());
79
80// 验证 # 真私有:类外访问是语法错误(不是类型错误)
81// 下面这行连解析都过不去,所以不能写在源码里:
82// const v = new Vault();
83// console.log(v.#value); // SyntaxError: Private field '#value' must be declared in an enclosing class
84//
85// 对比:TS 的 private 只是编译期检查,运行时能绕过
86const acc = new Account("a2", "x");
87console.log((acc as unknown as { secret: string }).secret); // "s",运行时确实能读到
1a1(rainboy) 100 alias s/r 1
21 3 1
3s
字段的种类
| 种类 | 写法 | 归属 |
|---|---|---|
| 实例字段 | balance = 0 | 每个实例一份 |
| 只读字段 | readonly id: string | 只能在声明处或构造函数里赋值 |
| 静态字段 | static total = 0 | 挂在类上,不在实例上 |
| 可选字段 | nickname?: string | 类型是 string | undefined |
两种"私有"
这是最容易混淆的地方:
| 写法 | 检查时机 | 能否运行时绕过 |
|---|---|---|
private secret | 只在编译期 | 能,运行时属性真实存在 |
#realSecret | 编译期 + 运行时 | 不能,是真正的私有槽 |
实测证明 private 只是编译期检查:
1class Account { private secret = "s"; }
2const a = new Account();
3console.log((a as any).secret); // "s" —— 运行时确实能读到
而 # 私有字段连解析都过不去:
1const v = new Vault();
2console.log(v.#value);
3// SyntaxError: Private field '#value' must be declared in an enclosing class
注意这是语法错误而不是类型错误——它连编译都到不了。所以:
- 需要"提醒同事别碰"→
private够了 - 需要"运行时真的访问不到"(安全边界)→ 必须用
#
参数属性
构造函数参数前加修饰符,会自动声明字段并赋值:
1class Point {
2 constructor(
3 public x: number,
4 private y: number,
5 readonly z: number = 0,
6 ) {}
7}
这是不可擦除语法——它生成真正的赋值代码(见 类型擦除)。编译产物里 this.x = x 是显式存在的。
getter / setter
1get label(): string { return `${this.id}(${this.owner})`; }
2set label(v: string) { this.nickname = v; }
一个细节:如果只有 get 没有 set,属性自动变成 readonly。这在 TypeScript 4.3 之后是默认行为。
继承、抽象类与 implements
1// 继承、抽象类、implements
2
3abstract class Shape {
4 // 抽象方法:子类必须实现
5 abstract area(): number;
6 abstract readonly kind: string;
7
8 // 普通方法:子类继承
9 describe(): string {
10 return `${this.kind} area=${this.area().toFixed(2)}`;
11 }
12
13 // 模板方法:定义流程,子类填细节
14 report(): string {
15 return `[${this.describe()}]`;
16 }
17}
18
19class Circle extends Shape {
20 readonly kind = "circle";
21 constructor(private radius: number) { super(); }
22 area(): number { return Math.PI * this.radius ** 2; }
23}
24
25class Square extends Shape {
26 readonly kind = "square";
27 constructor(private side: number) { super(); }
28 area(): number { return this.side ** 2; }
29}
30
31// 不能实例化抽象类
32// @ts-expect-error TS2511: 不能创建抽象类的实例
33new Shape();
34
35// implements:只检查实例侧的形状,不继承实现
36interface Serializable {
37 serialize(): string;
38}
39
40interface Comparable<T> {
41 compareTo(other: T): number;
42}
43
44class Version implements Serializable, Comparable<Version> {
45 constructor(private major: number, private minor: number) {}
46
47 serialize(): string {
48 return `${this.major}.${this.minor}`;
49 }
50
51 compareTo(other: Version): number {
52 return this.major - other.major || this.minor - other.minor;
53 }
54}
55
56// 类可以同时 extends 和 implements
57class NamedCircle extends Circle implements Serializable {
58 constructor(radius: number, private name: string) { super(radius); }
59 serialize(): string { return `${this.name}:${this.area().toFixed(2)}`; }
60}
61
62// implements 只检查形状:私有字段会导致"名义化"检查
63class HasPrivate {
64 private secret = "s";
65}
66// 下面这个类虽然结构相同,但因为 private 不同源,不能互相赋值
67class OtherPrivate {
68 private secret = "s";
69}
70// @ts-expect-error TS2322: 私有字段来源不同
71const hp: HasPrivate = new OtherPrivate();
72
73// override 关键字:显式标记覆写
74class Base {
75 greet(): string { return "base"; }
76}
77class Derived extends Base {
78 override greet(): string { return "derived"; }
79}
80
81const shapes: Shape[] = [new Circle(1), new Square(2)];
82console.log(shapes.map((s) => s.report()));
83console.log(new Version(1, 2).serialize(), new NamedCircle(1, "c").serialize());
84console.log(new Derived().greet());
1[ '[circle area=3.14]', '[square area=4.00]' ]
21.2 c:3.14
3derived
抽象类
1abstract class Shape {
2 abstract area(): number; // 子类必须实现
3 describe(): string { ... } // 子类继承
4}
抽象类有两个作用:
- 不能实例化:
new Shape()报error TS2511: Cannot create an instance of an abstract class. - 定义模板方法:父类定义流程(
report()调用describe()调用area()),子类只填area()
implements 检查什么
implements 只检查实例侧的形状,不继承任何实现:
1interface Serializable { serialize(): string }
2
3class Version implements Serializable, Comparable<Version> {
4 serialize(): string { ... } // 必须自己实现
5}
关键区别:
extends | implements | |
|---|---|---|
| 继承实现 | ✓ | ✗ |
| 继承类型 | ✓ | ✗(只检查) |
| 数量 | 只能一个 | 可以多个 |
| 目标 | 类 | 类或接口 |
private 破坏结构类型
TypeScript 通常是结构类型(看形状),但 private 字段是个例外:
1class HasPrivate { private secret = "s" }
2class OtherPrivate { private secret = "s" }
3
4// @ts-expect-error TS2322: 私有字段来源不同
5const hp: HasPrivate = new OtherPrivate();
两个类结构完全一样,但因为 private 来自不同的声明,TypeScript 认为它们不兼容。这是名义化(nominal)检查的少数场景。
override 关键字
显式标记覆写父类方法:
1class Derived extends Base {
2 override greet(): string { return "derived"; }
3}
override 本身是可选的,但配合 noImplicitOverride 开启后强制要求:
1class Derived extends Base {
2 greet(): string { return "d"; }
3 // error TS4114: This member must have an 'override' modifier because it overrides
4 // a member in the base class 'Base'.
5}
它能防止"父类方法改名后子类不再覆写它"的隐蔽 bug。
this 类型与泛型类
1// 类的 this 类型、泛型类、类与接口的关系
2
3// this 类型:返回 this 实现链式调用
4class QueryBuilder {
5 private parts: string[] = [];
6
7 select(cols: string): this {
8 this.parts.push(`select ${cols}`);
9 return this;
10 }
11
12 from(table: string): this {
13 this.parts.push(`from ${table}`);
14 return this;
15 }
16
17 build(): string {
18 return this.parts.join(" ");
19 }
20}
21
22class FilterBuilder extends QueryBuilder {
23 where(cond: string): this {
24 // 只能通过 public 方法访问 parts,这里用 build 演示
25 void cond;
26 return this;
27 }
28}
29
30const q = new QueryBuilder().select("*").from("users").build();
31// 继承链上 this 依然正确:FilterBuilder 的方法可链式调用父类方法
32const f = new FilterBuilder().select("*").where("id=1").build();
33
34// 泛型类 + 泛型约束
35interface Entity {
36 id: string;
37}
38
39class Repository<T extends Entity> {
40 private items = new Map<string, T>();
41
42 save(item: T): void {
43 this.items.set(item.id, item);
44 }
45
46 find(id: string): T | undefined {
47 return this.items.get(id);
48 }
49
50 all(): T[] {
51 return [...this.items.values()];
52 }
53}
54
55interface User extends Entity {
56 name: string;
57}
58
59const repo = new Repository<User>();
60repo.save({ id: "1", name: "rainboy" });
61
62// 泛型静态方法(静态方法不能用类的类型参数)
63class Box<T> {
64 constructor(private value: T) {}
65 get(): T { return this.value; }
66
67 // 静态方法必须自己声明类型参数
68 static of<U>(v: U): Box<U> {
69 return new Box(v);
70 }
71
72 // 下面这样写会报错:静态成员不能引用类类型参数
73 // static bad(v: T): Box<T> { return new Box(v); }
74}
75
76const box = Box.of(1);
77
78// 类实现接口时的可见性:implements 只要求 public 部分
79interface Readable {
80 read(): string;
81}
82class File implements Readable {
83 private content = "data";
84 read(): string { return this.content; } // 这个方法必须是 public
85}
86
87console.log(q, f);
88console.log(repo.find("1")?.name, repo.all().length, box.get(), new File().read());
1select * from users select *
2rainboy 1 1 data
this 类型实现链式调用
1class QueryBuilder {
2 select(cols: string): this { ...; return this; }
3 from(table: string): this { ...; return this; }
4}
返回类型写 this(而不是 QueryBuilder)的好处:子类继承后链式调用不丢类型。
1class FilterBuilder extends QueryBuilder {
2 where(cond: string): this { ...; return this; }
3}
4
5new FilterBuilder().select("*").where("id=1").build();
6// 如果 select 返回 QueryBuilder,这里 where 就调用不到了
泛型类
1class Repository<T extends Entity> {
2 save(item: T): void { }
3 find(id: string): T | undefined { }
4}
泛型参数绑定在实例上,创建时确定。
静态方法不能引用类的类型参数
1class Box<T> {
2 static of<U>(v: U): Box<U> { return new Box(v); } // 自己声明 U
3
4 static bad(v: T): Box<T> { }
5 // error TS2302: Static members cannot reference class type parameters.
6}
原因:静态成员属于类本身,而 T 属于实例。类存在时还没有实例,T 无从确定。静态方法需要泛型就自己声明一个。
装饰器:两条路线
装饰器是"在类定义时修改类行为"的语法。TypeScript 支持两套语义不同、不能混用的装饰器:
| legacy | 标准(TC39 stage-3) | |
|---|---|---|
| 配置 | experimentalDecorators: true | 无需配置 |
| 来源 | TypeScript 早期实验实现 | ECMAScript 标准提案 |
| 元数据 | emitDecoratorMetadata 支持 | 无 |
| 签名 | (target, key, descriptor) | (value, context) |
| 字段装饰器 | 不能改初始值 | 返回函数可改初始值 |
| 谁在用 | nestjs、TypeORM、Angular | 新项目、库作者 |
| TS 版本 | 一直支持 | TS 5.0+ |
同一个类不能既有 legacy 装饰器又有标准装饰器。experimentalDecorators 是全局开关,开了之后所有装饰器都按 legacy 语义解释。所以本仓库给两条路线各配了一份 tsconfig.json:
src/decorators-legacy/tsconfig.json→experimentalDecorators: truesrc/decorators-standard/tsconfig.json→ 无此选项
Legacy 装饰器
1// legacy 装饰器:类装饰器
2
3// 类装饰器接收构造函数,可以返回一个新类替换它
4function sealed<T extends new (...args: any[]) => object>(ctor: T): T {
5 Object.seal(ctor);
6 Object.seal(ctor.prototype);
7 return ctor;
8}
9
10// 不替换,只做副作用(常见于注册)
11function register(ctor: new (...args: any[]) => object): void {
12 console.log(`register: ${ctor.name}`);
13}
14
15@sealed
16@register
17class BugReport {
18 type = "report";
19 title: string;
20
21 constructor(t: string) {
22 this.title = t;
23 }
24}
25
26// 返回新类:替换原类
27function withTimestamp<T extends new (...args: any[]) => object>(ctor: T) {
28 return class extends ctor {
29 createdAt = new Date("2020-01-01");
30 };
31}
32
33@withTimestamp
34class Post {
35 title = "p";
36}
37
38const p = new Post();
39// 装饰器顺序:靠近类的先执行(register 先,然后 sealed)
40// 但工厂函数的求值顺序相反(从上到下)
41
42console.log(new BugReport("bug").title, p.title, (p as Post & { createdAt: Date }).createdAt.toISOString());
1register: BugReport
2bug p 2020-01-01T00:00:00.000Z
五种 legacy 装饰器
| 位置 | 签名 |
|---|---|
| 类 | (ctor) => ctor | void |
| 方法 | (target, key, descriptor) => descriptor | void |
| 属性 | (target, key) => void |
| 访问器 | (target, key, descriptor) => descriptor | void |
| 参数 | (target, key, parameterIndex) => void |
1// legacy 方法/属性/参数装饰器
2
3// 方法装饰器:三个参数 (target, propertyKey, descriptor)
4function log(
5 target: object,
6 propertyKey: string,
7 descriptor: PropertyDescriptor,
8): void {
9 const original = descriptor.value as (...args: unknown[]) => unknown;
10 descriptor.value = function (this: unknown, ...args: unknown[]) {
11 console.log(`call ${propertyKey}(${args.join(",")})`);
12 return original.apply(this, args);
13 };
14}
15
16// 属性装饰器:只有 (target, propertyKey),没有 descriptor
17function readonly(target: object, propertyKey: string): void {
18 Object.defineProperty(target, propertyKey, { writable: false });
19}
20
21// 参数装饰器:(target, propertyKey, parameterIndex)
22function inject(
23 target: object,
24 propertyKey: string | undefined,
25 parameterIndex: number,
26): void {
27 console.log(`inject into ${String(propertyKey)}[${parameterIndex}]`);
28}
29
30class Calculator {
31 @readonly
32 version = "1.0";
33
34 @log
35 add(a: number, b: number): number {
36 return a + b;
37 }
38
39 @log
40 greet(@inject name: string): string {
41 return `hi ${name}`;
42 }
43}
44
45const c = new Calculator();
46console.log(c.add(1, 2));
47console.log(c.greet("rainboy"));
48console.log(c.version);
49
50// 验证 readonly:属性装饰器用 defineProperty 让字段不可写
51// 注意:严格模式下赋值会抛错,非严格模式静默失败
52try {
53 (c as unknown as { version: string }).version = "2.0";
54 console.log("赋值后:", c.version);
55} catch (e) {
56 console.log("赋值被拦截:", (e as Error).message);
57}
1call add(1,2)
23
3call greet(rainboy)
4hi rainboy
51.0
6赋值后: 1.0
类装饰器可以替换类
1function withTimestamp<T extends new (...args: any[]) => object>(ctor: T) {
2 return class extends ctor {
3 createdAt = new Date("2020-01-01");
4 };
5}
6
7@withTimestamp
8class Post { title = "p"; }
9
10const p = new Post();
11p.createdAt;
12// error TS2339: Property 'createdAt' does not exist on type 'Post'.
类型不会跟着更新——这是 legacy 类装饰器的已知限制。要用到新字段必须断言:
1(p as Post & { createdAt: Date }).createdAt;
应用顺序
多个装饰器从下往上应用(靠近类/成员的先执行):
1@prefix("Mr. ") // 后执行
2@upperCase // 先执行
3get name() { return this._name; }
4// 结果:Mr. RAINBOY
但装饰器工厂的求值是从上往下:
1@factoryA() // 先求值(拿到装饰器函数)
2@factoryB() // 后求值
3class C {}
emitDecoratorMetadata
开启后,TypeScript 会把类型信息写进装饰器元数据:
1// emitDecoratorMetadata:装饰器能拿到类型元信息
2//
3// 重要:tsx / esbuild 不实现 emitDecoratorMetadata,所以这个文件必须用 tsc 编译再运行:
4// npx tsc -p content/typescript/src/decorators-legacy --outDir content/typescript/src/decorators-legacy/.emit --noEmit false
5// node content/typescript/src/decorators-legacy/.emit/metadata.js
6// 实测输出:find: params=[Number,String] return=Boolean
7// 用 tsx 跑同一文件:params=[] return=undefined(元数据没生成)
8//
9// 依赖 reflect-metadata(提供 Reflect.getMetadata / Reflect.metadata)
10
11import "reflect-metadata";
12
13function describe(target: object, propertyKey: string): void {
14 const paramTypes = Reflect.getMetadata("design:paramtypes", target, propertyKey);
15 const returnType = Reflect.getMetadata("design:returntype", target, propertyKey);
16 const names = paramTypes?.map((t: { name: string }) => t.name) ?? [];
17 console.log(`${propertyKey}: params=[${names}] return=${returnType?.name}`);
18}
19
20class Service {
21 @describe
22 find(id: number, name: string): boolean {
23 void id;
24 void name;
25 return true;
26 }
27}
28
29console.log(new Service().find(1, "a"));
三个可读的 key:
1Reflect.getMetadata("design:type", target, key) // 属性类型
2Reflect.getMetadata("design:paramtypes", target, key) // 参数类型数组
3Reflect.getMetadata("design:returntype", target, key) // 返回类型
tsx / esbuild 不实现 emitDecoratorMetadata这是实践中很容易踩的坑。同一个文件:
1# 用 tsc 编译后运行 —— 元数据存在
2$ npx tsc -p content/typescript/src/decorators-legacy --outDir .../.emit --noEmit false
3$ node .../.emit/metadata.js
4find: params=[Number,String] return=Boolean
5
6# 用 tsx 直接运行 —— 元数据丢失
7$ npx tsx content/typescript/src/decorators-legacy/metadata.ts
8find: params=[] return=undefined
reflect-metadata 是必需的运行时依赖,它提供 Reflect.getMetadata。nestjs 的依赖注入正是靠 design:paramtypes 拿到构造函数参数类型来决定注入什么——所以 nestjs 项目必须用 tsc(或配置了元数据的 swc)编译,不能用 esbuild/tsx 直接跑。
标准装饰器(TS 5.0+)
1// 标准装饰器(TC39 stage-3,TS 5.0+)
2// 不需要 experimentalDecorators
3
4// 方法装饰器:(value, context)
5function logged<T extends (...args: any[]) => any>(
6 fn: T,
7 ctx: ClassMethodDecoratorContext,
8): T {
9 return function (this: unknown, ...args: unknown[]) {
10 console.log(`call ${String(ctx.name)}`);
11 return fn.apply(this, args);
12 } as T;
13}
14
15// 装饰器工厂
16function withRetry(times: number) {
17 return function <T extends (...args: any[]) => any>(
18 fn: T,
19 ctx: ClassMethodDecoratorContext,
20 ): T {
21 return function (this: unknown, ...args: unknown[]) {
22 let last: unknown;
23 for (let i = 0; i < times; i++) {
24 try {
25 return fn.apply(this, args);
26 } catch (e) {
27 last = e;
28 }
29 }
30 throw last;
31 } as T;
32 };
33}
34
35class Calc {
36 @logged
37 add(a: number, b: number): number {
38 return a + b;
39 }
40
41 @withRetry(3)
42 flaky(failTimes: number): string {
43 // 用一个计数器模拟不稳定
44 if (this.attempts++ < failTimes) throw new Error("fail");
45 return "ok";
46 }
47
48 attempts = 0;
49}
50
51const c = new Calc();
52console.log(c.add(1, 2));
53console.log(c.flaky(2));
54console.log("context 提供的字段:", Calc.name);
1call add
23
3ok
4context 提供的字段: Calc
标准装饰器的签名是 (value, context),context 里有:
| 字段 | 含义 |
|---|---|
name | 成员名(可能是 symbol) |
kind | "class" / "method" / "field" / "getter" / "setter" / "accessor" |
static | 是否静态成员 |
private | 是否私有 |
metadata | 装饰器间传递数据的对象 |
access | { get, set, has },访问成员的能力 |
addInitializer | 注册初始化回调 |
1// 标准装饰器的 context 对象与各类装饰器
2
3// 1. 类装饰器:(value, context: ClassDecoratorContext)
4function tagged<T extends new (...args: any[]) => object>(
5 value: T,
6 ctx: ClassDecoratorContext,
7): T {
8 console.log(`class ${String(ctx.name)}, kind=${ctx.kind}`);
9 ctx.addInitializer(function (this: object) {
10 (this as { tagged?: boolean }).tagged = true;
11 });
12 return value;
13}
14
15// 2. 字段装饰器:返回初始化函数
16function double(_value: undefined, ctx: ClassFieldDecoratorContext) {
17 return function (this: unknown, initial: number) {
18 console.log(`field ${String(ctx.name)} 初始化`);
19 return initial * 2;
20 };
21}
22
23// 3. 访问器装饰器
24function loggedAccessor(
25 value: { get: () => unknown; set: (v: unknown) => void },
26 ctx: ClassAccessorDecoratorContext,
27) {
28 console.log(`accessor ${String(ctx.name)}`);
29 return {
30 get() { return value.get.call(this); },
31 set(v: unknown) { value.set.call(this, v); },
32 };
33}
34
35// 4. getter 装饰器:返回类型必须匹配原 getter
36function upper(
37 getter: (this: unknown) => string,
38 ctx: ClassGetterDecoratorContext,
39): (this: unknown) => string {
40 void ctx;
41 return function (this: unknown) {
42 return getter.call(this).toUpperCase();
43 };
44}
45
46// 5. 静态成员装饰器
47function staticLog<T extends (...args: any[]) => any>(
48 fn: T,
49 ctx: ClassMethodDecoratorContext,
50): T {
51 console.log(`static method ${String(ctx.name)}, static=${ctx.static}`);
52 return fn;
53}
54
55@tagged
56class Config {
57 @double
58 level = 1;
59
60 accessor name = "rainboy";
61
62 @upper
63 get title(): string {
64 return "hello";
65 }
66
67 @staticLog
68 static create(): Config {
69 return new Config();
70 }
71}
72
73const c = Config.create();
74console.log(c.level, c.name, c.title, (c as { tagged?: boolean }).tagged);
75
76// context 对象的字段(实测)
77console.log("context 字段: name, kind, static, private, metadata, access");
1static method create, static=true
2class Config, kind=class
3field level 初始化
42 rainboy HELLO undefined
5context 字段: name, kind, static, private, metadata, access
各类标准装饰器
1// 类:接收构造函数
2function tagged(value: T, ctx: ClassDecoratorContext) { return value; }
3
4// 方法:接收函数,返回替换函数
5function logged(fn: T, ctx: ClassMethodDecoratorContext) { return wrapper; }
6
7// 字段:返回初始化函数,可以改初始值
8function double(_v: undefined, ctx: ClassFieldDecoratorContext) {
9 return function (initial: number) { return initial * 2; };
10}
11
12// getter:返回类型必须匹配原 getter
13function upper(getter: (this: unknown) => string, ctx: ClassGetterDecoratorContext) {
14 return function (this: unknown) { return getter.call(this).toUpperCase(); };
15}
注意 context 类型的准确名称:是 ClassAccessorDecoratorContext(不是 ClassAutoAccessorDecoratorContext),这个笔误很容易犯。
addInitializer 的执行时机
这是标准装饰器最需要实测的一点:
| 装饰器位置 | addInitializer 执行时机 | this 是 |
|---|---|---|
| 类装饰器 | 类定义时执行一次 | 类本身 |
| 字段/方法装饰器 | 每个实例构造时 | 实例 |
实测验证:
1装饰器执行, this 是 class: App
2class 级 initializer 执行, this = 类本身
3--- 创建实例 1 ---
4field 级 initializer 执行, this 是实例: true
5--- 创建实例 2 ---
6field 级 initializer 执行, this 是实例: true
所以给实例加属性必须用字段装饰器的 addInitializer:
1// 错误:类装饰器里的 this 是类,不是实例
2function withInit(value: T, ctx: ClassDecoratorContext) {
3 ctx.addInitializer(function () {
4 (this as any).ready = true; // 挂到类上了,实例拿不到
5 });
6}
7
8// 正确:字段装饰器的 this 是实例
9function perInstance(_v: undefined, ctx: ClassFieldDecoratorContext) {
10 ctx.addInitializer(function () {
11 (this as any).ready = true; // 每个实例都有
12 });
13}
两条路线的差异对照
1// legacy 与标准装饰器的语义差异(同一场景两种写法)
2
3// ============ 差异 1:装饰器函数签名 ============
4// legacy: (target, propertyKey, descriptor)
5// 标准: (value, context)
6// context 里有 name / kind / static / private / metadata / addInitializer
7
8// ============ 差异 2:this 的处理 ============
9// 标准装饰器里 this 是 unknown,必须显式声明
10
11// ============ 差异 3:字段装饰器返回值含义 ============
12// legacy 字段装饰器无返回值(不能改初始值)
13// 标准字段装饰器返回一个初始化函数,可以改初始值
14function legacyStyle(target: object, key: string): void {
15 void target; void key;
16}
17function standardStyle(_v: undefined, ctx: ClassFieldDecoratorContext) {
18 return function (initial: string) {
19 return `${initial}!`; // 可以改初始值
20 };
21}
22
23// ============ 差异 4:addInitializer 的执行时机(关键差异)============
24// 实测结论:
25// - 类装饰器里的 addInitializer:在【类定义时】执行一次,this 是类本身
26// - 字段/方法装饰器里的 addInitializer:在【每个实例构造时】执行,this 是实例
27function withInit<T extends new (...args: any[]) => object>(
28 value: T,
29 ctx: ClassDecoratorContext,
30): T {
31 ctx.addInitializer(function (this: unknown) {
32 // 注意:这里 this 是类,不是实例!所以下面这样写没用
33 console.log(`[class initializer] ${String(ctx.name)}, this === 类: ${this === value}`);
34 });
35 return value;
36}
37
38// 要给每个实例加属性,得用字段装饰器的 addInitializer
39function perInstance(_v: undefined, ctx: ClassFieldDecoratorContext) {
40 ctx.addInitializer(function (this: unknown) {
41 (this as { ready?: boolean }).ready = true; // 这里 this 才是实例
42 });
43}
44
45// legacy 要这样写(对比):
46function legacyWithInit<T extends new (...args: any[]) => object>(ctor: T): T {
47 const orig = ctor;
48 return class extends orig {
49 constructor(...args: any[]) {
50 super(...args);
51 (this as { ready?: boolean }).ready = true;
52 }
53 };
54}
55
56// ============ 差异 5:元数据 ============
57// legacy + emitDecoratorMetadata 可拿到类型(依赖 reflect-metadata)
58// 标准装饰器没有元数据机制,要自己实现
59
60@withInit
61class App {
62 @standardStyle
63 name = "app";
64
65 @perInstance
66 ready = false;
67}
68
69const a = new App();
70console.log(a.name, (a as { ready?: boolean }).ready);
71
72// ============ 差异 6:tsconfig ============
73// legacy: "experimentalDecorators": true(可加 emitDecoratorMetadata)
74// 标准: 什么都不用配;两者不能同时用于同一个类
75console.log("legacyStyle/legacyWithInit 仅作对比,未使用:", typeof legacyStyle, typeof legacyWithInit);
1[class initializer] App, this === 类: true
2app! true
3legacyStyle/legacyWithInit 仅作对比,未使用: function function
| 维度 | legacy | 标准 |
|---|---|---|
| 装饰器签名 | (target, key, descriptor) | (value, context) |
| 改字段初始值 | 不支持 | 返回函数即可 |
| 给实例加属性 | 返回子类 hack | 字段装饰器 addInitializer |
| 类型元数据 | emitDecoratorMetadata | 无,需自己实现 |
this 类型 | 隐式 any | unknown,必须显式声明 |
| 工具链兼容 | 只有 tsc 完整支持 | esbuild/swc 均支持 |
该选哪条
选 legacy 的情况:
- 用 nestjs / TypeORM / Angular(它们的依赖注入依赖
design:paramtypes元数据) - 已有大量 legacy 装饰器代码
选标准的情况:
- 新项目,不需要运行时类型元数据
- 需要构建速度(esbuild/swc 支持标准装饰器,但不支持
emitDecoratorMetadata) - 想用
accessor关键字和addInitializer
如果你想用 esbuild/swc 的高速构建,又想用装饰器,只能选标准装饰器。这是 nestjs 生态目前的结构性约束——它需要元数据,所以必须走 tsc 或专门配置的 swc。详见 运行 TypeScript。
常见错误速查
| 错误码 | 消息 | 触发场景 |
|---|---|---|
| TS2511 | Cannot create an instance of an abstract class. | new 抽象类 |
| TS2322 | Type 'OtherPrivate' is not assignable to type 'HasPrivate'. | private 字段来源不同 |
| TS2302 | Static members cannot reference class type parameters. | 静态成员引用类类型参数 |
| TS2339 | Property 'createdAt' does not exist on type 'Post'. | 类装饰器替换类后类型未更新 |
| TS2540 | Cannot assign to 'x' because it is a read-only property. | 只有 getter 没有 setter |
| TS4114 | This member must have an 'override' modifier... | 开启 noImplicitOverride 后漏写 override |
| TS2552 | Cannot find name 'ClassAutoAccessorDecoratorContext'. | context 类型名写错 |
| TS1270 | Decorator function return type ... is not assignable to type ... | 标准装饰器返回类型不匹配 |
小结
| 主题 | 核心结论 |
|---|---|
private vs # | 前者只编译期,后者运行时真私有 |
| 参数属性 | 不可擦除,生成赋值代码 |
| 抽象类 | 不能实例化,用于模板方法模式 |
implements | 只检查形状,不继承实现 |
this 类型 | 链式调用返回 this,子类不丢类型 |
| 静态成员 | 不能引用类的类型参数 |
| legacy 装饰器 | 5 种签名,支持元数据,nestjs 依赖它 |
| 标准装饰器 | (value, context),addInitializer 时机随位置不同 |
| 两者关系 | 全局互斥,experimentalDecorators 一开全变 legacy |
下一篇:工程化。