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 的依赖注入靠 design:paramtypes 拿到构造函数参数类型,决定注入什么。所以 nestjs 项目必须用 tsc(或配置了元数据的 swc)编译,不能用 esbuild/tsx 直接跑。
这是 nestjs 生态的结构性约束,不是配置问题。
为什么还在用
尽管有标准装饰器,legacy 仍在广泛使用:
| 原因 | 说明 |
|---|---|
| 生态依赖 | nestjs、TypeORM、Angular、class-validator 都用它 |
| 元数据能力 | 标准装饰器没有 emitDecoratorMetadata 的对应物 |
| 类替换 | legacy 类装饰器能返回新类,标准装饰器不能 |
与标准装饰器的选择
见 装饰器(标准)。简单规则:
- 用 nestjs / TypeORM / Angular → 只能 legacy
- 新项目,不需要元数据 → 标准装饰器(工具链兼容性更好)
- 想用 esbuild/swc 高速构建 + 装饰器 → 只能标准装饰器
相关
- 装饰器(标准) —— TC39 标准装饰器与差异对照
- 类与装饰器 —— 类成员与两种路线的完整对比
- 运行 TypeScript —— 工具链兼容性
- TypeScript 版本现状 —— TS 7 对工具链的影响