装饰器
TypeScript 5.x 已实现 TC39 阶段 3 装饰器提案,在现代项目中可以直接使用标准语义的装饰器,无需额外的编译开关(只要编译目标支持类字段)。装饰器是定义阶段执行的函数,可对**类、类成员(方法/访问器/字段)**添加横切逻辑。
如果你的生态仍依赖旧版的“实验性装饰器”(如 Angular、TypeORM 等),请在
tsconfig.json
中开启"experimentalDecorators": true
(并通常搭配"emitDecoratorMetadata": true
)以启用 legacy decorators。下面先介绍标准语义,再总结 legacy 差异与迁移提示。
Stage 3 装饰器要点
Stage 3 装饰器函数的签名为 (value, context) => newValue | void
:
value
是被修饰的类或成员定义。context
提供当前成员的元信息,例如名称、是静态还是实例、成员种类等。- 返回值(可选)可以替换原定义;如果不返回内容,则保持原实现。
- 装饰器在类声明完成时执行一次,不会在实例化时重复执行。
TypeScript 5.0+ 内置了多种 Class*DecoratorContext
类型,常见如下:
成员类型 | 上下文类型 |
---|---|
类 | ClassDecoratorContext |
实例/静态方法 | ClassMethodDecoratorContext |
getter/setter | ClassGetterDecoratorContext / ClassSetterDecoratorContext |
字段 | ClassFieldDecoratorContext |
accessor 声明 | ClassAccessorDecoratorContext |
此外,context
提供 kind
('class'、'method'、'field' 等)、name
、static
、private
、access
以及 addInitializer
等属性,用来在定义阶段注入元信息或附加逻辑。核心字段释义如下:
kind
:断言装饰器的适用场景(类/方法/字段/访问器)。name
:当前成员的名称(私有成员返回可读描述或undefined
)。static
与private
:帮助区分静态/私有成员,常用于条件分支。access
:提供get
/set
/has
辅助函数,以在实例上安全读取或写入值(包括私有字段)。addInitializer(fn)
:推迟执行到实例构造(或静态初始化)阶段,常用于注册依赖或填充额外属性。
function traceField(_value: undefined, context: ClassFieldDecoratorContext) {
context.addInitializer(function () {
const current = context.access.get?.call(this);
console.log(`实例 ${String(context.name)} 初始化为`, current);
});
return function (initialValue: unknown) {
console.log(`定义 ${String(context.name)} =`, initialValue);
return initialValue;
};
}
typescript
使用 addInitializer
为实例补充能力
function withLogger(value: Function, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
context.addInitializer(function () {
Reflect.defineProperty(this, `${methodName}Logs`, {
value: [] as string[],
writable: false,
});
});
return function wrapped(this: any, ...args: unknown[]) {
const entry = `${methodName}(${JSON.stringify(args)})`;
(this as Record<string, string[]>)[`${methodName}Logs`].push(entry);
return value.apply(this, args);
};
}
class AuditService {
@withLogger
fetchUser(id: number) {
return { id };
}
}
const service = new AuditService();
service.fetchUser(42);
console.log(service.fetchUserLogs); // ['fetchUser([42])']
typescript
addInitializer
的代码在构造函数开头执行一次,因此即便为私有方法或静态方法添加日志,也能保证辅助属性按需注入。
利用 accessor 扩展可观察状态
accessor
声明与装饰器结合可实现响应式或校验场景:
function reactive(value: { get(): unknown; set(v: unknown): void }, context: ClassAccessorDecoratorContext) {
const subscribers = new Set<() => void>();
const property = String(context.name);
const registerKey = `watch${property[0]?.toUpperCase()}${property.slice(1)}`;
context.addInitializer(function () {
Reflect.defineProperty(this, registerKey, {
value: (listener: () => void) => subscribers.add(listener),
});
});
return {
get() {
return value.get.call(this);
},
set(next: unknown) {
const prev = value.get.call(this);
if (Object.is(prev, next)) return;
value.set.call(this, next);
subscribers.forEach((notify) => notify());
},
};
}
class Store {
@reactive accessor count = 0;
}
const store = new Store();
store.watchCount(() => console.log('count changed'));
store.count = 1;
typescript
装饰器通过 value.get/set
复用原访问器逻辑,再借助 addInitializer
动态注入订阅方法,同时避免破坏私有存储语义。
方法和访问器示例
function logInvocation(value: Function, context: ClassMethodDecoratorContext) {
return function wrapped(this: unknown, ...args: unknown[]) {
console.log(`call → ${String(context.name)}`);
return value.apply(this, args);
};
}
class Greeter {
@logInvocation
greet(name: string) {
return `Hello ${name}`;
}
}
typescript
字段与 accessor 示例
function double(_value: undefined, context: ClassFieldDecoratorContext) {
return function (initialValue: number) {
console.log(`定义阶段 ${String(context.name)} = ${initialValue}`);
return initialValue * 2;
};
}
class Counter {
@double
count = 1;
}
console.log(new Counter().count); // 2
typescript
如果需要包装 getter/setter,可同时处理二者或使用 accessor
:
function clamp(min: number, max: number) {
return function (value: any, context: ClassAccessorDecoratorContext) {
const { get, set } = value;
return {
get() {
return get.call(this);
},
set(newValue: number) {
set.call(this, Math.max(min, Math.min(max, newValue)));
},
};
};
}
class Player {
@clamp(0, 100)
accessor health = 50;
}
typescript
类装饰器
类装饰器可以增添静态元信息或通过 context.addInitializer
注入实例初始化逻辑:
function stamped(version: string) {
return function (value: Function, context: ClassDecoratorContext) {
context.addInitializer(function () {
Reflect.defineProperty(this, Symbol.for('version'), { value: version });
});
};
}
@stamped('1.0.0')
class Service {}
typescript
Stage 3 装饰器不再支持参数装饰器,也不会暴露
property descriptor
。如果需要这些能力,参见下一节的 legacy 说明。
元数据与反射
当前 emitDecoratorMetadata
仍只针对 legacy 装饰器生成设计期元数据。若你依赖 reflect-metadata
自动推断参数类型,需要继续使用 legacy 模式或手动声明类型。
Stage 3 装饰器工厂、组合与执行顺序
- 装饰器工厂:和旧语法一样,可以返回一个实际执行的装饰器函数,只是入参不同。
function tag(label: string) { return (value: Function, context: ClassDecoratorContext) => { context.addInitializer(function () { Reflect.defineProperty(this, 'tag', { value: label }); }); }; } @tag('service') class TaggedService {}
typescript - 组合:多个装饰器依旧按照“先收集工厂、后执行装饰器”的顺序工作;执行顺序为从下至上。
function log(label: string) { return (value: any, context: ClassMethodDecoratorContext) => { console.log(`decorate ${String(context.name)} with ${label}`); return value; }; } class Pipeline { @log('second') @log('first') run() {} } // 打印顺序:decorate run with first → decorate run with second
typescript - 执行阶段:装饰器在类定义结束后立即运行,且仅执行一次;如需访问实例,可借助
context.addInitializer
或返回新的 accessor。
Stage 3 装饰器常见模式
日志 & 性能度量
:围绕方法包装performance.now()
、console.time
等逻辑,沉淀统一的调用追踪。缓存
:利用Map
/WeakMap
缓存纯函数结果,避免在业务类中手写记忆化代码。约束验证
:在 setter/accessor 中插入范围限制、schema 校验,确保对象状态始终有效。依赖注入
:结合context.addInitializer
将实例注册到容器或Reflect
元数据,使类之间解耦。
function measure(value: Function, context: ClassMethodDecoratorContext) {
const label = `${context.static ? 'static ' : ''}${String(context.name)}`;
return function instrumented(this: unknown, ...args: unknown[]) {
const start = performance.now();
try {
return value.apply(this, args);
} finally {
const duration = (performance.now() - start).toFixed(2);
console.log(`[${label}] took ${duration}ms`);
}
};
}
class ReportService {
@measure
generate() {
// heavy work
}
}
typescript
随着提案进入 Stage 3,Babel、tsc、Vite 等主流工具链已经内建标准装饰器支持,跨框架(NestJS、Vue Class API、Lit)生态也在逐步提供 Stage 3 版本或对应迁移指南。
Legacy 装饰器速览("experimentalDecorators": true
)
TypeScript 5.x 仍提供 legacy 装饰器以兼容既有生态。此模式下:
- 装饰器基于旧的提案语义,
experimentalDecorators
选项必须开启。 - 支持参数装饰器以及直接操作属性描述符;方法装饰器形参为
(target, propertyKey, descriptor)
。 emitDecoratorMetadata
可生成运行时可读的设计类型信息(多数依赖 Reflect Metadata 的库都使用这一点)。- 多个装饰器的执行顺序:装饰器工厂由上至下调用;装饰器函数本身由下至上执行。
迁移建议:
- 新代码优先使用 Stage 3 装饰器与显式类型声明,必要时在装饰器内部手动注入元信息或工厂函数,避免长期依赖
emitDecoratorMetadata
。 - 现有框架若仍依赖 legacy 行为,可暂时开启
experimentalDecorators
,同时在 CI 中提示相关用例,跟踪官方弃用时间线。 - 混用两套语义时务必在代码审查说明中标记差异(如
@legacy
前缀或注释),并为遗留装饰器附上迁移计划与负责人。
更多示例可参考 TypeScript 官方迁移指南及各框架的升级说明。
Legacy 语法详解(保留旧版结构)
- 基本语法:使用
@decorator
直接修饰声明,装饰器本质是(target, key, descriptor?) => void
形态的函数。function setProp(target: any) { // ... } @setProp class Demo {}
typescript - 装饰器工厂:返回一个函数,调用
@factory()
时先执行工厂再执行装饰器。function setProp(value: string) { return function (target: any, key?: string) { Reflect.defineMetadata('prop', value, target, key!); }; } class Sample { @setProp('name') title!: string; }
typescript - 装饰器组合:与旧笔记一致,工厂按书写顺序执行、装饰器函数反向执行。需要注意
emitDecoratorMetadata
会在每个装饰器注入设计类型信息。 - 执行顺序:
- 参数装饰器、方法装饰器、访问器装饰器、属性装饰器(实例成员)。
- 参数装饰器、方法装饰器、访问器装饰器、属性装饰器(静态成员)。
- 构造函数参数装饰器。
- 类装饰器。
Legacy 示例扩展(保留旧版本案例)
以下示例沿用早期笔记中的写法,演示 experimentalDecorators
模式下的语法:
function setName() {
console.log('get setName');
return function (target: any) {
console.log('setName');
};
}
function setAge() {
console.log('get setAge');
return function (target: any) {
console.log('setAge');
};
}
@setName()
@setAge()
class LegacyUser {}
typescript
类装饰器:
function logClass(target: Function) {
console.log('类装饰器触发:', target.name);
}
@logClass
class Person {
constructor(public name: string) {}
}
typescript
方法装饰器:
function enumerable(value: boolean) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor,
) {
descriptor.enumerable = value;
};
}
class Info {
private _name = 'toimc';
@enumerable(false)
get name() {
return this._name;
}
}
typescript
参数装饰器:
function required(target: any, propertyName: string, index: number) {
console.log(`修饰的是 ${propertyName} 的第 ${index + 1} 个参数`);
}
class Interview {
getInfo(prefix: string, @required field: string) {
console.log(prefix, field);
}
}
new Interview().getInfo('字段', 'age');
typescript
Legacy 元数据与校验示例
import 'reflect-metadata';
const REQUIRED_METADATA_KEY = Symbol('required');
function Required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
const existing: number[] = Reflect.getOwnMetadata(REQUIRED_METADATA_KEY, target, propertyKey) ?? [];
existing.push(parameterIndex);
Reflect.defineMetadata(REQUIRED_METADATA_KEY, existing, target, propertyKey);
}
function Validate(target: any, propertyName: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
descriptor.value = function (...args: unknown[]) {
const required: number[] = Reflect.getOwnMetadata(REQUIRED_METADATA_KEY, target, propertyName) ?? [];
required.forEach((index) => {
if (args[index] === undefined) {
throw new Error(`Missing required argument at position ${index}`);
}
});
return method.apply(this, args);
};
}
class LegacyBooking {
@Validate
book(title: string, @Required date?: string) {
console.log(`${title} → ${date ?? '待定'}`);
}
}
new LegacyBooking().book('一对一面试'); // 抛出缺失参数错误
typescript
reflect-metadata
结合 emitDecoratorMetadata
可以保留类型信息供运行时读取,这是 Stage 3 版本暂不提供的能力;迁移前应梳理依赖元数据的模块,并评估是否能以显式类型代替。
混合项目策略
- 在 Nuxt/Vite 等现代工具链中,启用 Stage 3 语法通常只需要
target
≥ES2022
;如仍需 legacy 装饰器,可通过 Babel@babel/plugin-proposal-decorators
(legacy 模式)或vue-cli
的experimentalDecorators
选项逐文件启用。 - 对 NestJS、TypeORM、class-transformer 等依赖
reflect-metadata
的库,可先升级到官方提供的 Stage 3 兼容分支,或在tsconfig.json
中拆分tsconfig.legacy.json
专门编译旧接口。 - 在 ESLint/TSLint 中配置自定义规则(例如禁止新增 legacy 装饰器),并在 PR 模板中要求标注装饰器语义,帮助团队迭代到统一风格。
- 跨端 SDK 建议新增装饰器适配层:在 Stage 3 环境暴露
decorate()
函数,在 legacy 环境继续导出@Legacy
,以降低调用方的语法差异。
这些示例需要在 tsconfig.json
中开启 "experimentalDecorators": true
才能编译,通过对比可以更好地理解 Stage 3 与 legacy 模式的差异。
参考资料
↑