TypeScript 装饰器经历了一段不太平滑的演进。早期版本(5.0 之前)使用的是实验性的"传统装饰器"(experimental decorators),需要在 tsconfig.json 中开启 experimentalDecorators: true。5.0 之后开始支持 ECMAScript 标准的装饰器提案(Stage 3),默认不兼容。
目前大部分遗留项目仍然在用传统装饰器,新项目可以逐步采用标准装饰器。本文以传统装饰器为例来说明模式——这些模式在新标准下同样适用,只是语法和参数有差异。
四种装饰器的签名
传统装饰器有四类,每种接收的参数不同:
// 类装饰器
type ClassDecorator = (target: Function) => void | Function
// 方法装饰器
type MethodDecorator = <T>(
target: T,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<any>
) => void | TypedPropertyDescriptor<any>
// 访问器装饰器(getter/setter)
type AccessorDecorator = <T>(
target: T,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<any>
) => void | TypedPropertyDescriptor<any>
// 属性装饰器
type PropertyDecorator = (
target: Object,
propertyKey: string | symbol
) => void
方法装饰器和属性装饰器虽然都接收 target 和 propertyKey,但方法装饰器额外接收属性描述符(descriptor),所以可以修改方法的行为。属性装饰器没有 descriptor,只能做元数据附加。
模式 1:自动日志(方法装饰器)
最常用的装饰器模式——给方法加日志,不侵入业务逻辑:
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
descriptor.value = function (...args: any[]) {
console.log(`[${new Date().toISOString()}] ${propertyKey} called with:`, args)
const start = performance.now()
const result = originalMethod.apply(this, args)
const duration = performance.now() - start
console.log(`[${new Date().toISOString()}] ${propertyKey} completed in ${duration.toFixed(2)}ms`)
return result
}
return descriptor
}
class UserService {
@log
async getUser(id: string) {
// 实际的业务逻辑
return db.users.find(id)
}
@log
async updateUser(id: string, data: Partial<User>) {
return db.users.update(id, data)
}
}
输出:
[2026-06-04T10:00:00.000Z] getUser called with: ["123"]
[2026-06-04T10:00:00.150Z] getUser completed in 150.23ms
这种模式的优点是所有方法的日志行为一致,修改日志格式只需要改一个装饰器。而且每个装饰器是独立的——可以给重要方法加日志,给高频方法跳过。
模式 2:输入校验(方法装饰器 + 元数据)
给方法参数加校验前置条件:
type ValidationRule = (value: any) => string | null // null 表示通过
function validate(rules: Record<string, ValidationRule>) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
descriptor.value = function (...args: any[]) {
// 通过参数名匹配规则(需要额外元数据来映射参数名)
const paramNames = getParamNames(originalMethod)
for (const [name, rule] of Object.entries(rules)) {
const index = paramNames.indexOf(name)
if (index === -1) continue
const error = rule(args[index])
if (error !== null) {
throw new ValidationError(`Validation failed for ${propertyKey}.${name}: ${error}`)
}
}
return originalMethod.apply(this, args)
}
return descriptor
}
}
class UserController {
@validate({
email: (v: any) => typeof v === 'string' && v.includes('@') ? null : 'Invalid email',
age: (v: any) => typeof v === 'number' && v >= 0 && v <= 150 ? null : 'Invalid age'
})
async createUser(email: string, age: number) {
// ...
}
}
这里不需要在每个方法体里手写校验逻辑。如果将来校验规则变了,只需要改装饰器参数。
模式 3:方法节流/防抖(方法装饰器)
在 UI 事件或 API 调用场景下,节流和防抖是常见需求:
function throttle(ms: number) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
let lastCall = 0
descriptor.value = function (...args: any[]) {
const now = Date.now()
if (now - lastCall < ms) return
lastCall = now
return originalMethod.apply(this, args)
}
return descriptor
}
}
class AnalyticsTracker {
@throttle(1000) // 每秒最多上报一次
trackEvent(name: string, data: Record<string, any>) {
// 发送分析事件
navigator.sendBeacon('/api/analytics', JSON.stringify({ name, data }))
}
}
模式 4:依赖注入(类装饰器 + 属性装饰器)
依赖注入是类级别装饰器的典型应用。先定义一个容器:
const container = new Map<string, any>()
function Injectable() {
return function (target: any) {
// 标记这个类可以被注入
Reflect.defineMetadata('injectable', true, target)
}
}
function Inject(token: string) {
return function (target: any, propertyKey: string | symbol) {
// 在属性上存储依赖标记
Reflect.defineMetadata('inject', token, target, propertyKey)
}
}
// 自动装配
function autoInject<T extends { new (...args: any[]): any }>(target: T): T {
return class extends target {
constructor(...args: any[]) {
super(...args)
// 遍历原型链,查找需要注入的属性
const instance = this as any
const proto = Object.getPrototypeOf(instance)
for (const key of Object.getOwnPropertyNames(proto)) {
const token = Reflect.getMetadata('inject', proto, key)
if (token && container.has(token)) {
instance[key] = container.get(token)
}
}
}
}
}
@Injectable()
class Logger {
log(msg: string) { console.log(msg) }
}
@Injectable()
@autoInject
class UserServiceDI {
@Inject('Logger')
private logger!: Logger
async getUser(id: string) {
this.logger.log(`Fetching user ${id}`)
// ...
}
}
实际项目一般用 reflect-metadata 库来处理元数据操作。这里的关键是:属性装饰器只能标记"需要注入什么",具体的注入逻辑在类装饰器(autoInject)中执行。
传统装饰器 vs 标准装饰器
TypeScript 5.0 开始支持 ECMAScript 标准装饰器。主要区别:
| 方面 | 传统装饰器(experimental) | 标准装饰器(Standard) |
|---|---|---|
| tsconfig 配置 | 需要 experimentalDecorators: true | 不需要 |
| 方法装饰器参数 | (target, key, descriptor) | (target, context) |
| 返回值 | descriptor 或无 | 新函数或 void |
| 元数据支持 | reflect-metadata | 需要装饰器自行管理 |
| 与 Babel 兼容 | 不一致 | 一致 |
标准装饰器的方法装饰器签名:
function logged<T extends (...args: any[]) => any>(
target: (this: void, ...args: Parameters<T>) => ReturnType<T>,
context: ClassMethodDecoratorContext
) {
const methodName = String(context.name)
function replacementMethod(this: any, ...args: any[]) {
console.log(`LOG: Entering method '${methodName}'.`)
const result = target.call(this, ...args)
console.log(`LOG: Exiting method '${methodName}'.`)
return result
}
return replacementMethod
}
context 对象提供了 name、kind、static、private、addInitializer 等信息,比传统装饰器的签名更丰富。
总结
| 装饰器类型 | 能做的事 | 典型用途 |
|---|---|---|
| 类装饰器 | 替换或包装构造函数 | 依赖注入、注册到框架 |
| 方法装饰器 | 修改方法行为 | 日志、校验、重试、缓存 |
| 访问器装饰器 | 修改 getter/setter | 响应式计算、延迟初始化 |
| 属性装饰器 | 附加元数据 | 注入标记、序列化映射 |
装饰器的核心价值在于横切关注点(cross-cutting concerns)的分离。日志、校验、权限检查这类逻辑不适合散落在每个方法体内——它们横跨多个方法,最好用一个装饰器统一管理。使用装饰器时要注意的一个原则是:它应该增强方法但不改变方法的语义,否则会引入难以调试的隐式行为。


