装饰器(标准)

2026-09-09 00:00    #typescript  

标准装饰器(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 类型隐式 anyunknown,必须显式声明
工具链兼容只有 tsc 完整支持esbuild/swc 均支持

该选哪条

选标准装饰器:

选 legacy:

两者的根本分歧是"元数据"

标准装饰器没有 emitDecoratorMetadata 的对应机制——TC39 的提案里没有类型元数据这一环(因为 JavaScript 没有类型)。

所以依赖注入框架要么继续用 legacy + reflect-metadata,要么自己实现元数据机制(如显式写 @Inject("token"))。这是 nestjs 至今无法迁移到标准装饰器的根本原因。

相关