装饰器(legacy)

2026-09-09 00:00    #typescript  

legacy 装饰器是 TypeScript 早期的实验实现,需要 experimentalDecorators: true。它在 ECMAScript 标准化之前就存在,与标准装饰器语义不同。nestjs、TypeORM、Angular 都依赖它。

配置

1{
2  "compilerOptions": {
3    "experimentalDecorators": true,
4    "emitDecoratorMetadata": true
5  }
6}

emitDecoratorMetadata 是可选的,它让装饰器能读取类型信息(需要 reflect-metadata)。

这是全局开关

experimentalDecorators: true 会让所有装饰器按 legacy 语义解释,不能只对部分类生效。所以标准装饰器和 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// 类装饰器接收构造函数,可以返回一个新类替换它
 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

类装饰器可以返回一个新类来替换原类:

 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'.
类型不会跟着更新

类装饰器替换了类,但类型定义不变。要用新字段必须断言:

1(p as Post & { createdAt: Date }).createdAt;

这是 legacy 装饰器的已知限制。标准装饰器没有这个问题(但它也不能替换类)。

方法装饰器

 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

方法装饰器接收 descriptor,可以包装原方法:

1function log(target: object, key: string, descriptor: PropertyDescriptor): void {
2  const original = descriptor.value as (...args: unknown[]) => unknown;
3  descriptor.value = function (this: unknown, ...args: unknown[]) {
4    console.log(`call ${key}(${args.join(",")})`);
5    return original.apply(this, args);
6  };
7}

属性装饰器

没有 descriptor 参数——这是和其他装饰器的区别:

1function readonly(target: object, key: string): void {
2  Object.defineProperty(target, key, { writable: false });
3}

因为属性装饰器只能拿到 target 和 key,它无法改变初始值。这是 legacy 装饰器的一个能力缺口(标准装饰器补上了这一点)。

访问器装饰器

 1// 访问器装饰器:装饰 get/set
 2
 3// 注意:访问器装饰器里改 descriptor.configurable 对 getter 不一定生效,
 4// 因为 getter 定义在原型上,configurable 默认就是 true。
 5// 这里演示更实用的场景:包装 getter 的返回值。
 6
 7function upperCase(
 8  target: object,
 9  propertyKey: string,
10  descriptor: PropertyDescriptor,
11): void {
12  const original = descriptor.get;
13  if (original) {
14    descriptor.get = function (this: unknown) {
15      const v = original.call(this);
16      return typeof v === "string" ? v.toUpperCase() : v;
17    };
18  }
19}
20
21// 工厂形式:带参数
22function prefix(text: string) {
23  return function (
24    target: object,
25    propertyKey: string,
26    descriptor: PropertyDescriptor,
27  ): void {
28    const original = descriptor.get;
29    if (original) {
30      descriptor.get = function (this: unknown) {
31        return `${text}${String(original.call(this))}`;
32      };
33    }
34  };
35}
36
37class Person {
38  private _name = "rainboy";
39
40  @prefix("Mr. ")
41  @upperCase
42  get name(): string {
43    return this._name;
44  }
45}
46
47const p = new Person();
48// 装饰器从下往上应用:先 upperCase,再 prefix
49console.log(p.name);
1Mr. RAINBOY

可以包装 getter:

1function upperCase(target: object, key: string, descriptor: PropertyDescriptor): void {
2  const original = descriptor.get;
3  if (original) {
4    descriptor.get = function (this: unknown) {
5      return String(original.call(this)).toUpperCase();
6    };
7  }
8}

应用顺序

多个装饰器从下往上应用(靠近声明的先执行):

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)   // 返回类型

需要 reflect-metadata 提供运行时支持。

实测:工具链兼容性

这是实践中最容易踩的坑:

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
工具装饰器本身emitDecoratorMetadata
tsc✓✓
tsx / esbuild✓✗
swc(需配置)✓✓(需显式开)
这决定了 nestjs 项目的构建方式

nestjs 的依赖注入靠 design:paramtypes 拿到构造函数参数类型,决定注入什么。所以 nestjs 项目必须用 tsc(或配置了元数据的 swc)编译,不能用 esbuild/tsx 直接跑。

这是 nestjs 生态的结构性约束,不是配置问题。

为什么还在用

尽管有标准装饰器,legacy 仍在广泛使用:

原因说明
生态依赖nestjs、TypeORM、Angular、class-validator 都用它
元数据能力标准装饰器没有 emitDecoratorMetadata 的对应物
类替换legacy 类装饰器能返回新类,标准装饰器不能

与标准装饰器的选择

见 装饰器(标准)。简单规则:

相关