标准装饰器(TC39 stage-3)从 TS 5.0 起支持,不需要任何配置。它的签名是 (value, context),语义与 legacy 装饰器不同。
配置
不需要配置。不要开 experimentalDecorators——开了之后标准装饰器会被按 legacy 语义解释。
context 对象
标准装饰器的第二个参数是 context,包含:
| 字段 | 含义 |
|---|---|
name | 成员名(可能是 string 或 symbol) |
kind | "class" / "method" / "field" / "getter" / "setter" / "accessor" |
static | 是否静态成员 |
private | 是否私有 |
metadata | 装饰器之间传递数据的对象 |
access | { get, set, has },访问成员的能力 |
addInitializer | 注册初始化回调 |
各类装饰器
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
方法装饰器
1function logged<T extends (...args: any[]) => any>(
2 fn: T,
3 ctx: ClassMethodDecoratorContext,
4): T {
5 return function (this: unknown, ...args: unknown[]) {
6 console.log(`call ${String(ctx.name)}`);
7 return fn.apply(this, args);
8 } as T;
9}
注意 this 是 unknown,必须显式声明(legacy 里是隐式 any)。
字段装饰器可以改初始值
这是标准装饰器相对 legacy 的能力增强:
1function double(_value: undefined, ctx: ClassFieldDecoratorContext) {
2 return function (this: unknown, initial: number) {
3 return initial * 2; // 可以改初始值
4 };
5}
字段装饰器返回一个函数,这个函数在实例初始化时被调用,返回值作为新的初始值。
getter 装饰器
返回类型必须匹配原 getter:
1function upper(
2 getter: (this: unknown) => string,
3 ctx: ClassGetterDecoratorContext,
4): (this: unknown) => string {
5 return function (this: unknown) {
6 return getter.call(this).toUpperCase();
7 };
8}
context 类型名容易写错
访问器装饰器的 context 是 ClassAccessorDecoratorContext,不是 ClassAutoAccessorDecoratorContext。实测报错:
1error TS2552: Cannot find name 'ClassAutoAccessorDecoratorContext'.
2Did you mean 'ClassAccessorDecoratorContext'?
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
实测结论:
| 装饰器位置 | 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}
与 legacy 的差异对照
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 | 标准 |
|---|---|---|
| 配置 | experimentalDecorators: true | 无需配置 |
| 签名 | (target, key, descriptor) | (value, context) |
| 改字段初始值 | ✗ 不支持 | ✓ 返回函数即可 |
| 给实例加属性 | 返回子类 hack | 字段装饰器 addInitializer |
| 类替换 | ✓ 可返回新类 | ✗ 不能替换 |
| 类型元数据 | emitDecoratorMetadata | ✗ 无,需自己实现 |
this 类型 | 隐式 any | unknown,必须显式声明 |
| 工具链兼容 | 只有 tsc 完整支持 | esbuild/swc 均支持 |
该选哪条
选标准装饰器:
- 新项目
- 不需要运行时类型元数据
- 想用 esbuild/swc 的高速构建
- 想用
accessor关键字
选 legacy:
- 用 nestjs / TypeORM / Angular
- 需要
design:paramtypes元数据做依赖注入 - 需要类装饰器替换类
两者的根本分歧是"元数据"
标准装饰器没有 emitDecoratorMetadata 的对应机制——TC39 的提案里没有类型元数据这一环(因为 JavaScript 没有类型)。
所以依赖注入框架要么继续用 legacy + reflect-metadata,要么自己实现元数据机制(如显式写 @Inject("token"))。这是 nestjs 至今无法迁移到标准装饰器的根本原因。
相关
- 装饰器(legacy) —— legacy 装饰器详解
- 类与装饰器 —— 类成员与两条路线总览
- 运行 TypeScript —— 工具链兼容性