装饰器详解
TypeScript装饰器与元编程
阅读提示:正文以代码和白话为主,不出现类型论公式。进阶文档中若出现
Γ ⊢ e : τ这类记号,第一遍可完全跳过(完整规则见001-HowToReadThisCourse)。
前置知识
- 泛型约束与默认值:建议先完成前一篇的学习
学习目标
- 掌握「1. 历史动机与发展脉络」的核心机制、典型用法与常见陷阱
- 掌握「2. 形式化定义」的核心机制、典型用法与常见陷阱
- 掌握「3. 理论推导与原理解析」的核心机制、典型用法与常见陷阱
- 掌握「4. 代码示例」的核心机制、典型用法与常见陷阱
- 掌握「5. 对比分析」的核心机制、典型用法与常见陷阱
本篇系统阐述 TypeScript 装饰器的形式语义、演进脉络、企业级用法与陷阱,覆盖 TS 1.5 实验性装饰器到 TS 5.0 标准化装饰器的完整历程,对标 MIT 6.5838、Stanford CS242、CMU 15-814 等课程对 meta-programming 与 aspect-oriented programming 的教学要求。
1. 历史动机与发展脉络
1.1 元编程与横切关注点的需求
面向对象编程(OOP)擅长封装业务核心逻辑,但在日志、事务、权限、缓存等横切关注点上常导致代码重复。Gregor Kiczales 等人在 1997 年 Xerox PARC 工作中提出 Aspect-Oriented Programming(AOP,面向切面编程),将横切关注点模块化为 aspects。装饰器正是 AOP 在 JavaScript/TypeScript 中的实现载体。
1.2 TypeScript 装饰器演进时间线
| 版本 | 年份 | 关键特性 | 设计动机 |
|---|---|---|---|
| TS 1.5 | 2015 | 引入实验性装饰器(experimentalDecorators) | 借鉴 Java 注解与 Python 装饰器,支持 Angular 2.0 |
| TS 1.6 | 2015 | emitDecoratorMetadata 配合 Reflect.metadata | 为依赖注入提供类型元数据 |
| TS 4.0 | 2020 | 装饰器元组语义明确化 | 改善与 Angular、NestJS 兼容性 |
| TS 5.0 | 2023 | 正式支持 TC39 Stage 3 装饰器 | 不再需要 experimentalDecorators,原生支持;同时落地 Symbol.metadata、context.access 与 addInitializer |
截至 2026-09,TC39 装饰器提案仍处于 Stage 3(未定稿),TS 5.x 后续版本对装饰器本身无重大改动;标准装饰器当前形态不含参数装饰器。
1.3 TC39 标准化进程
ECMAScript 装饰器提案历经多次迭代:
- Stage 1(2014):Yehuda Katz 与 Jonathan Turner 最初提案,强 reflect-based 模型
- Stage 2(2016):提案进入 Stage 2,此后由 Daniel Ehrenberg 等多次重写为更简洁的 hook-based 模型
- Stage 3(2022):Ron Buckton 与 Kristen Hewell 推进,TS 5.0 实现此版本;截至 2026-09 仍未进入 Stage 4
1.4 元编程理论基础
装饰器属于 meta-programming(元编程)范畴。从类型论角度,装饰器是类型构造子到类型构造子的高阶函数:
形式化地,类 经装饰器 处理后得到 ,满足:
即装饰后的类应是原类的子类型,确保 LSP 不被破坏。
2. 形式化定义
2.1 实验性装饰器签名(TS 1.5 - 4.x)
实验性装饰器采用 positional parameters 风格:
// 类装饰器
type ClassDecorator<TFunction extends Function> = (
target: TFunction,
) => TFunction | void;
// 方法装饰器
type MethodDecorator = <T>(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>,
) => TypedPropertyDescriptor<T> | void;
// 属性装饰器
type PropertyDecorator = (
target: Object,
propertyKey: string | symbol,
) => void;
// 参数装饰器
type ParameterDecorator = (
target: Object,
propertyKey: string | symbol | undefined,
parameterIndex: number,
) => void;
2.2 标准化装饰器签名(TS 5.0+)
TC39 Stage 3 装饰器采用 context-based 风格,更清晰且类型安全:
type DecoratorContext =
| ClassMemberDecoratorContext
| ClassMethodDecoratorContext
| ClassGetterDecoratorContext
| ClassSetterDecoratorContext
| ClassFieldDecoratorContext
| ClassAccessorDecoratorContext
| ClassDecoratorContext;
interface ClassMethodDecoratorContext<
Class = unknown,
Method extends Function = Function,
> {
readonly kind: 'method';
readonly name: string | symbol;
readonly access: { has(object: Class): boolean; get(object: Class): Method };
readonly static: boolean;
readonly addInitializer(initializer: (this: Class) => void): void;
}
2.3 语义规则
标准化装饰器的求值规则形式化:
装饰器组合满足 right-associative(右结合):
执行顺序:
- 自顶向下:装饰器求值从上到下(先
A,后B,再C) - 自底向上:装饰器应用从下到上(先
C(X),后B(C(X)),再A(B(C(X))))
2.4 元数据模型
2.4.1 实验性元数据(Reflect.metadata)
依赖 reflect-metadata polyfill:
import 'reflect-metadata';
@Reflect.metadata('design:type', Function)
class A {
@Reflect.metadata('design:paramtypes', [String])
method(@Reflect.metadata('design:type', String) name: string) {}
}
2.4.2 标准化元数据(Symbol.metadata)
TS 5.2+ 使用 Symbol.metadata:
// 元数据通过 context.metadata 注入
function track(className: string) {
return function <T extends new (...args: any[]) => any>(
target: T,
context: ClassDecoratorContext<T>,
): T | void {
context.metadata.set('className', className);
return target;
};
}
@track('UserService')
class UserService {}
// 读取元数据
const meta = (UserService as any)[Symbol.metadata];
console.log(meta.get('className')); // 'UserService'
3. 理论推导与原理解析
3.1 装饰器作为高阶函数
装饰器的本质是高阶函数。考虑:
function logged<T extends (...args: any[]) => any>(
originalMethod: T,
context: ClassMethodDecoratorContext,
): T {
return function (this: any, ...args: Parameters<T>): ReturnType<T> {
console.log(`Calling ${String(context.name)}`);
return originalMethod.apply(this, args);
} as T;
}
数学表达:
这是 演算中的 function composition(函数组合):
3.2 装饰器组合的代数结构
多个装饰器组合形成 monoid(幺半群):
- 单位元:
identity装饰器 - 二元运算:组合
- 结合律:
- 单位律:
这为装饰器组合提供了数学保证。
3.3 装饰器求值与应用的分离
TypeScript 编译器将装饰器分为两阶段:
- 求值阶段(top-down):装饰器表达式本身求值,得到装饰器函数
- 应用阶段(bottom-up):装饰器函数依次应用到目标
形式化:
这与 compose 函数的语义一致:
const compose = (...fns: Function[]) => (x: any) => fns.reduceRight((v, f) => f(v), x);
3.4 元数据传播的不变量
装饰器元数据必须满足 idempotency(幂等性):
实践中,Reflect.defineMetadata 满足此不变量,但跨继承链时需注意:
子类继承父类元数据,但同名元数据子类覆盖父类。
4. 代码示例
4.1 企业级依赖注入容器
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"useDefineForClassFields": false,
"lib": ["ES2022", "DOM"],
"outDir": "dist",
"declaration": true
}
}
di-container.ts — TS 5.0+ 标准装饰器实现:
/**
* 类型安全的依赖注入容器
* 利用 TS 5.0 标准装饰器与 Symbol.metadata
* 不依赖 reflect-metadata polyfill
*/
// 元数据键
const INJECTABLE_KEY = Symbol('injectable');
interface InjectableMetadata {
scope: 'singleton' | 'transient';
token: symbol;
}
// 容器
export class DIContainer {
private static instances = new Map<symbol, unknown>();
private static registry = new Map<symbol, new (...args: any[]) => unknown>();
static register<T extends new (...args: any[]) => unknown>(
token: symbol,
target: T,
): void {
this.registry.set(token, target);
}
static resolve<T>(token: symbol): T {
if (this.instances.has(token)) {
return this.instances.get(token) as T;
}
const Target = this.registry.get(token);
if (!Target) throw new Error(`No provider for token ${String(token)}`);
// 构造器不接收依赖:依赖通过字段注入完成(见下方 Inject)
const instance = new Target();
this.instances.set(token, instance);
return instance as T;
}
}
// Injectable 装饰器(类装饰器):登记 token 并注册进容器
export function Injectable(options: { scope?: 'singleton' | 'transient'; token?: symbol } = {}) {
return function <T extends new (...args: any[]) => unknown>(
target: T,
context: ClassDecoratorContext<T>,
): T {
const token = options.token ?? Symbol(target.name);
const metadata: InjectableMetadata = {
scope: options.scope ?? 'singleton',
token,
};
// context.metadata 是可变的普通对象(Record),直接以键值对写入
context.metadata[INJECTABLE_KEY] = metadata;
DIContainer.register(token, target);
return target;
};
}
// Inject 装饰器(字段装饰器)
// 注意:标准装饰器(TC39 Stage 3 当前形态)不含参数装饰器,
// 构造器参数注入是 legacy experimentalDecorators + emitDecoratorMetadata 的能力。
// 标准装饰器下的等价做法:用字段装饰器把 token 记入元数据,
// 并返回初始化函数,在实例化时解析依赖并作为字段初始值。
export function Inject(token: symbol) {
return function (
_initialValue: undefined,
context: ClassFieldDecoratorContext<unknown, unknown>,
): (initialValue: unknown) => unknown {
context.metadata[`inject:${String(context.name)}`] = token;
return function () {
// 初始化函数在每次实例化时执行:解析容器中的单例并赋给字段
return DIContainer.resolve(token);
};
};
}
// 使用示例:token 必须是共享的同一符号(Symbol() 每次调用都创建新值,需提取为常量)
const LoggerToken = Symbol('Logger');
const UserServiceToken = Symbol('UserService');
@Injectable({ token: LoggerToken })
class Logger {
log(msg: string) { console.log(`[LOG] ${msg}`); }
}
@Injectable({ token: UserServiceToken })
class UserService {
// 字段注入:实例化时由容器解析 Logger 并赋值
@Inject(LoggerToken)
private logger!: Logger;
getUser(id: string) {
this.logger.log(`Getting user ${id}`);
return { id, name: 'Alice' };
}
}
const userService = DIContainer.resolve<UserService>(UserServiceToken);
userService.getUser(1);
// 输出:[LOG] Getting user 1
4.2 方法装饰器:性能监控与日志
/**
* 方法级 AOP 装饰器
* 提供性能监控、日志、错误捕获、重试
*/
interface Logger {
info(msg: string, meta?: Record<string, unknown>): void;
error(msg: string, err?: Error): void;
}
// 性能监控装饰器(TS 5.0+ 标准签名)
export function measure(logger?: Logger) {
return function <T extends (...args: any[]) => any>(
originalMethod: T,
context: ClassMethodDecoratorContext<any, T>,
): T {
const methodName = String(context.name);
return async function (this: any, ...args: Parameters<T>): Promise<ReturnType<T>> {
const start = performance.now();
try {
const result = await originalMethod.apply(this, args);
const duration = performance.now() - start;
logger?.info(`Method ${methodName} completed`, { duration, args });
return result;
} catch (err) {
const duration = performance.now() - start;
logger?.error(`Method ${methodName} failed after ${duration}ms`, err as Error);
throw err;
}
} as T;
};
}
// 重试装饰器
export function retry(maxAttempts = 3, delayMs = 1000) {
return function <T extends (...args: any[]) => Promise<any>>(
originalMethod: T,
context: ClassMethodDecoratorContext<any, T>,
): T {
return async function (this: any, ...args: Parameters<T>): Promise<Awaited<ReturnType<T>>> {
let lastError: unknown;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await originalMethod.apply(this, args);
} catch (err) {
lastError = err;
if (attempt < maxAttempts) {
await new Promise((r) => setTimeout(r, delayMs * attempt));
}
}
}
throw lastError;
} as T;
};
}
// 使用示例
class PaymentService {
@measure()
@retry(3, 500)
async processPayment(amount: number): Promise<{ success: boolean }> {
// 模拟可能失败的网络请求
if (Math.random() < 0.3) throw new Error('Network error');
return { success: true };
}
}
4.3 属性装饰器:自动校验
/**
* 属性校验装饰器
* 利用 TS 5.0 ClassFieldDecoratorContext 实现
*/
interface ValidationRule {
validate(value: unknown): boolean;
message: string;
}
class RequiredRule implements ValidationRule {
constructor(public message = 'Value is required') {}
validate(value: unknown): boolean {
return value !== null && value !== undefined && value !== '';
}
}
class MinLengthRule implements ValidationRule {
constructor(public min: number, public message?: string) {}
validate(value: unknown): boolean {
return typeof value === 'string' && value.length >= this.min;
}
get messageStr() {
return this.message ?? `Must be at least ${this.min} characters`;
}
}
const VALIDATION_KEY = Symbol('validation');
export function validate(...rules: ValidationRule[]) {
return function <T>(
target: undefined | T,
context: ClassFieldDecoratorContext<any, T>,
): (this: any, initialValue: T | undefined) => T {
// 注册规则到类元数据
context.metadata.set(`validation:${String(context.name)}`, rules);
return function (this: any, initialValue: T | undefined): T {
const value = (initialValue ?? ({} as T)) as T;
for (const rule of rules) {
if (!rule.validate(value)) {
throw new Error(`Validation failed for ${String(context.name)}: ${rule.message}`);
}
}
return value;
};
};
}
// 校验工具
export function validateObject<T extends object>(obj: T): { valid: boolean; errors: string[] } {
const errors: string[] = [];
const ctor = obj.constructor as any;
const metadata = ctor[Symbol.metadata];
if (!metadata) return { valid: true, errors };
for (const key in obj) {
const rules = metadata.get(`validation:${key}`) as ValidationRule[] | undefined;
if (!rules) continue;
for (const rule of rules) {
if (!rule.validate((obj as any)[key])) {
errors.push(`${key}: ${rule.message}`);
}
}
}
return { valid: errors.length === 0, errors };
}
// 使用示例
class UserDTO {
@validate(new RequiredRule(), new MinLengthRule(3))
name: string = '';
@validate(new RequiredRule('Email is required'))
email: string = '';
@validate(new MinLengthRule(8))
password: string = '';
}
const user = new UserDTO();
user.name = 'ab'; // 太短
const result = validateObject(user);
console.log(result); // { valid: false, errors: ['name: Must be at least 3 characters', ...] }
4.4 类装饰器:自动注册路由(NestJS 风格)
/**
* 自动注册路由的类装饰器
* 模拟 NestJS Controller 装饰器
*/
interface RouteDefinition {
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
path: string;
handler: string;
}
const ROUTES_KEY = Symbol('routes');
export function Controller(prefix = '') {
return function <T extends new (...args: any[]) => any>(
target: T,
context: ClassDecoratorContext<T>,
): T {
const routes = (context.metadata.get(ROUTES_KEY) as RouteDefinition[]) ?? [];
const fullRoutes = routes.map((r) => ({ ...r, path: prefix + r.path }));
context.metadata.set(ROUTES_KEY, fullRoutes);
return target;
};
}
export function Get(path = '') {
return function (
target: any,
context: ClassMethodDecoratorContext,
): void {
const existing = context.metadata.get(ROUTES_KEY) as RouteDefinition[] ?? [];
existing.push({ method: 'GET', path, handler: String(context.name) });
context.metadata.set(ROUTES_KEY, existing);
};
}
export function Post(path = '') {
return function (
target: any,
context: ClassMethodDecoratorContext,
): void {
const existing = context.metadata.get(ROUTES_KEY) as RouteDefinition[] ?? [];
existing.push({ method: 'POST', path, handler: String(context.name) });
context.metadata.set(ROUTES_KEY, existing);
};
}
// 使用示例
@Controller('/api/users')
class UserController {
@Get('/')
list() {
return [];
}
@Get('/:id')
getById() {
return null;
}
@Post('/')
create() {
return { id: 1 };
}
}
// 读取路由元数据
const routes = (UserController as any)[Symbol.metadata]?.get(ROUTES_KEY);
console.log(routes);
// [
// { method: 'GET', path: '/api/users/', handler: 'list' },
// { method: 'GET', path: '/api/users/:id', handler: 'getById' },
// { method: 'POST', path: '/api/users/', handler: 'create' }
// ]
4.5 Accessor 装饰器:响应式状态
/**
* 响应式状态装饰器
* 类似 MobX @observable,利用 TS 5.0 ClassAccessorDecoratorContext
*/
type Listener = () => void;
export function observable<T>(
target: ClassAccessorDecoratorTarget<any, T>,
context: ClassAccessorDecoratorContext<any, T>,
): ClassAccessorDecoratorResult<any, T> {
const listeners = new Set<Listener>();
return {
get(this: any): T {
return target.get.call(this);
},
set(this: any, value: T): void {
target.set.call(this, value);
listeners.forEach((fn) => fn());
},
init(this: any, initialValue: T): T {
// 注册订阅 API(简化示例)
(this as any)[`__subscribe_${String(context.name)}`] = (fn: Listener) => {
listeners.add(fn);
return () => listeners.delete(fn);
};
return initialValue;
},
};
}
// 使用示例
class Store {
@observable
accessor count = 0;
@observable
accessor name = 'Alice';
}
const store = new Store();
const unsubscribe = (store as any).__subscribe_count(() => {
console.log('count changed:', store.count);
});
store.count = 1; // 输出: count changed: 1
unsubscribe();
store.count = 2; // 无输出
4.6 实验性装饰器完整示例(向后兼容)
/**
* 实验性装饰器示例(TS 1.5 - 4.x,需 experimentalDecorators)
* 适用于仍维护老项目的场景
*/
import 'reflect-metadata';
function LogClass<T extends { new (...args: any[]): {} }>(constructor: T): T {
return class extends constructor {
constructor(...args: any[]) {
super(...args);
console.log(`Instance of ${constructor.name} created`);
}
};
}
function LogMethod(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor,
): PropertyDescriptor {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[LOG] ${propertyKey} called with`, args);
const result = original.apply(this, args);
console.log(`[LOG] ${propertyKey} returned`, result);
return result;
};
return descriptor;
}
function DefaultValue(value: any) {
return function (target: any, propertyKey: string) {
let storedValue = value;
Object.defineProperty(target, propertyKey, {
get() { return storedValue; },
set(newValue) { storedValue = newValue; },
enumerable: true,
configurable: true,
});
};
}
function Required(target: any, propertyKey: string, parameterIndex: number) {
const existingRequired: number[] =
Reflect.getOwnMetadata('required', target, propertyKey) ?? [];
existingRequired.push(parameterIndex);
Reflect.defineMetadata('required', existingRequired, target, propertyKey);
}
function ValidateParams(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor,
): PropertyDescriptor {
const original = descriptor.value;
const requiredParams: number[] =
Reflect.getOwnMetadata('required', target, propertyKey) ?? [];
descriptor.value = function (...args: any[]) {
requiredParams.forEach((idx) => {
if (args[idx] === undefined || args[idx] === null) {
throw new Error(`Parameter at index ${idx} of ${propertyKey} is required`);
}
});
return original.apply(this, args);
};
return descriptor;
}
@LogClass
class ExampleService {
@DefaultValue('default')
name: string;
@ValidateParams
process(@Required data: unknown, @Required callback: Function) {
return callback(data);
}
}
5. 对比分析
5.1 与 Java 注解对比
// Java 注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Loggable {
String value() default "";
}
public class Service {
@Loggable("Processing")
public void process() { /* ... */ }
}
// 需要反射或 AOP 框架(如 Spring AOP)处理
| 维度 | TypeScript 装饰器 | Java 注解 |
|---|---|---|
| 运行时 | 装饰器是真实函数,直接修改目标 | 注解是元数据,需反射读取 |
| 处理框架 | 不需要额外框架 | 需要 Spring AOP / AspectJ |
| 类型安全 | 完整类型检查 | 注解类型检查,但反射弱类型 |
| 元数据 | Symbol.metadata(TS 5.2+) | @Retention(RUNTIME) 反射 |
| 性能 | 装饰器在加载时执行一次 | 反射开销 |
| 生态 | NestJS、TypeORM、MikroORM | Spring、Hibernate、JPA |
5.2 与 Python 装饰器对比
# Python 装饰器
def log(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
class Service:
@log
def process(self):
pass
# Python 装饰器是真正的运行时高阶函数
| 维度 | TypeScript 装饰器 | Python 装饰器 |
|---|---|---|
| 类型系统 | 静态类型,完整类型检查 | 动态类型,无静态检查(除非 mypy) |
| 作用域 | 类成员专用 | 任意函数/类 |
| 元数据 | Symbol.metadata | functools.wraps 保留属性 |
| 性能 | 编译期展开 | 运行时包装 |
| 表达力 | 仅类相关 | 更通用(也可装饰函数) |
5.3 与 C# Attribute 对比
// C# Attribute
[AttributeUsage(AttributeTargets.Method)]
public class LogAttribute : Attribute { }
public class Service {
[Log]
public void Process() { }
}
// 需反射读取 attribute,类似 Java
| 维度 | TypeScript 装饰器 | C# Attribute |
|---|---|---|
| 语义 | 代码变换(运行时函数) | 仅元数据标记 |
| 处理方式 | 装饰器函数直接执行 | 需反射 + 框架处理 |
| 表达力 | 强(可修改目标) | 弱(仅标记) |
| 静态分析 | 类型检查 | 编译期检查 attribute 用法 |
5.4 与 Rust Macro 对比
// Rust macro 1.2 (proc-macro)
#[derive(Debug, Clone)]
struct Point { x: i32, y: i32 }
// 自定义 attribute macro
#[log_call]
fn process() { /* ... */ }
| 维度 | TypeScript 装饰器 | Rust Macro |
|---|---|---|
| 执行时机 | 编译期 + 运行时 | 完全编译期 |
| 类型安全 | 类型检查 | 类型检查 |
| 性能 | 运行时函数包装 | 零成本抽象(编译期展开) |
| 复杂度 | 较低 | 高(需 proc-macro 体系) |
| 生态 | NestJS、TypeORM | serde、tokio、axum |
5.5 与 Scala Annotations 对比
// Scala
@deprecated("use newMethod instead", "2.0")
def oldMethod(): Unit = {}
// 类似 Java 注解,需反射处理
6. 常见陷阱与最佳实践
6.1 陷阱:实验性 vs 标准装饰器混用
// 错误:实验性与标准装饰器不能混用
{
"compilerOptions": {
"experimentalDecorators": true, // 启用实验性
// 标准装饰器将无法使用
}
}
最佳实践:新项目使用 TS 5.0+ 标准装饰器,关闭 experimentalDecorators;旧项目逐步迁移。
6.2 陷阱:装饰器返回类型不匹配
// 实验性装饰器返回错误类型
function badDecorator<T extends new (...args: any[]) => any>(target: T): string {
return 'wrong'; // 错误:应返回 T 或 void
}
// 标准装饰器
function goodDecorator<T extends new (...args: any[]) => any>(
target: T,
context: ClassDecoratorContext<T>,
): T {
return class extends target {}; // 返回子类
}
6.3 陷阱:方法装饰器中 this 丢失
function badLog<T extends (...args: any[]) => any>(
method: T,
context: ClassMethodDecoratorContext,
): T {
return function (...args: any[]) {
console.log(this); // 错误:箭头函数无 this
return method(...args);
} as T;
}
function goodLog<T extends (...args: any[]) => any>(
method: T,
context: ClassMethodDecoratorContext,
): T {
return function (this: any, ...args: any[]) {
console.log(this); // OK
return method.apply(this, args);
} as T;
}
6.4 陷阱:emitDecoratorMetadata 依赖 polyfill
// 实验性装饰器 + emitDecoratorMetadata 需要 reflect-metadata
import 'reflect-metadata'; // 必须导入
// 否则 Reflect.getMetadata 未定义
最佳实践:TS 5.2+ 使用 Symbol.metadata 替代,无需 polyfill。
6.5 陷阱:装饰器顺序与组合语义
@A
@B
class X {}
// 求值顺序:A → B(自顶向下)
// 应用顺序:B(X) → A(B(X))(自底向上)
// 最终:A(B(X))
6.6 陷阱:any 类型污染
// 反模式
function Log(target: any, key: string, descriptor: any) {
// any 类型丢失所有检查
}
最佳实践:使用标准装饰器 context 类型,或显式泛型约束。
6.7 陷阱:私有成员与装饰器
实验性装饰器对私有成员支持有限,TS 5.0+ 通过 context.access 提供安全访问:
function logAccess<T>(
target: any,
context: ClassFieldDecoratorContext<any, T>,
) {
return function (this: any, value: T): T {
console.log(`Setting ${String(context.name)}`, value);
return value;
};
}
class Example {
@logAccess
#privateField: number = 0; // TS 5.0+ 支持私有字段装饰器
}
6.8 陷阱:装饰器与 useDefineForClassFields
{
"compilerOptions": {
"useDefineForClassFields": true // 影响属性初始化语义
}
}
TS 5.0+ 装饰器与 useDefineForClassFields: true 协同良好,但实验性装饰器可能出现属性初始化顺序问题。
7. 工程实践
7.1 tsc 编译配置
// 标准装饰器(TS 5.0+)
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "ESNext.Disposable"],
"outDir": "dist",
"declaration": true
}
}
// 实验性装饰器(向后兼容)
{
"compilerOptions": {
"target": "ES2021",
"module": "CommonJS",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"useDefineForClassFields": false
}
}
7.2 装饰器调试技巧
// 1. 打印 context 查看元数据
function debug<T>(target: T, context: any): T {
console.log('Context:', context);
console.log('Metadata:', context.metadata);
return target;
}
// 2. 使用 tsc --declaration 查看 .d.ts 中装饰器签名
// tsc --declaration --emitDeclarationOnly
// 3. 使用 source-map 调试编译后代码
// tsc --sourceMap
7.3 装饰器单元测试
import { describe, it, expect } from 'vitest';
describe('@measure decorator', () => {
it('should log duration', async () => {
const logs: string[] = [];
const logger: Logger = {
info: (msg) => logs.push(msg),
error: (msg) => logs.push(msg),
};
class Service {
@measure(logger)
async method() { return 42; }
}
const result = await new Service().method();
expect(result).toBe(42);
expect(logs[0]).toContain('method completed');
});
});
7.4 性能基准测试
// 装饰器在加载时执行一次,运行时仅 wrapper 开销
// 标准装饰器比实验性装饰器快约 5-10%(无 polyfill)
// benchmark.ts
import { Bench } from 'tinybench';
const bench = new Bench();
class Plain { method() { return 1; } }
class Decorated {
@measure()
method() { return 1; }
}
bench.add('Plain', () => new Plain().method());
bench.add('Decorated', () => new Decorated().method());
await bench.run();
console.table(bench.table());
7.5 ESLint 规则
// .eslintrc.cjs
module.exports = {
rules: {
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-module-boundary-types': 'error',
// 装饰器专用规则
'eslint-plugin-decorator-position/decorator-newline': 'error',
},
};
8. 案例研究
8.1 NestJS 的依赖注入体系
NestJS 是 TypeScript 装饰器应用的标杆项目。其核心 @Module、@Injectable、@Controller 装饰器构建了完整的 DI 体系:
// NestJS 简化版
@Module({
providers: [UserService, Logger],
controllers: [UserController],
})
export class AppModule {}
@Injectable()
class UserService {
constructor(@Inject(Logger) private logger: Logger) {}
}
@Controller('/users')
class UserController {
@Get('/:id')
@UseGuards(AuthGuard)
getUser(@Param('id') id: string) {
return this.userService.getUser(id);
}
}
收益:相比手写工厂模式,DI 容器+装饰器让代码量减少约 40%,模块边界清晰,测试时可轻松 mock。
8.2 TypeORM 的实体装饰器
@Entity({ name: 'users' })
@Index(['email'], { unique: true })
class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ length: 100 })
@Length(3, 100)
name: string;
@Column({ unique: true })
email: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@OneToMany(() => Post, (post) => post.author)
posts: Post[];
@ManyToMany(() => Role)
@JoinTable()
roles: Role[];
}
TypeORM 通过装饰器将类型系统与数据库 schema 绑定,实现类型安全的 ORM。
8.3 Angular 的变更检测
Angular 2.0+ 大量使用装饰器定义组件、指令、管道:
@Component({
selector: 'app-user',
template: `<h1>{{ user.name }}</h1>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
class UserComponent {
@Input() user: User;
@Output() userChange = new EventEmitter<User>();
@HostListener('click', ['$event'])
onClick(event: MouseEvent) {
this.userChange.emit(this.user);
}
}
Angular 编译器(ngc)在装饰器基础上做 AOT(Ahead-of-Time)编译优化。
8.4 MikroORM 的实体映射
MikroORM 利用 TS 5.0 标准装饰器与 Reflect.metadata 实现类型安全的实体定义:
@Entity()
class Book {
@PrimaryKey()
id!: number;
@Property()
title!: string;
@ManyToOne()
author!: Author;
@ManyToMany()
tags = new Collection<Tag>(this);
}
8.5 TypeStack 的 class-validator
import { IsString, IsInt, Min, Max, validate } from 'class-validator';
class CreateUserDTO {
@IsString()
name: string;
@IsInt()
@Min(0)
@Max(150)
age: number;
}
const dto = new CreateUserDTO();
dto.name = 'Alice';
dto.age = 200;
const errors = await validate(dto);
// [{ property: 'age', constraints: { max: 'age must not be greater than 150' } }]
填空题知识点讲解
题目 4:实验性装饰器需要导入的 polyfill 是 ______。
解析讲解:reflect-metadata
题目 5:TS 5.0 标准装饰器中,方法装饰器的 context.kind 是 ______。
解析讲解:'method'
题目 6:装饰器组合的最终效果 @A @B class X 等价于函数调用 ______。
解析讲解:A(B(X))
编程题知识点讲解
题目 7:实现一个 @memoize 方法装饰器,缓存方法返回值(基于参数),并在 TS 5.0+ 标准装饰器语法下编写。
解析讲解:
export function memoize<T extends (...args: any[]) => any>(
method: T,
context: ClassMethodDecoratorContext<any, T>,
): T {
const cache = new Map<string, ReturnType<T>>();
const keyOf = (args: any[]) => JSON.stringify(args);
return function (this: any, ...args: Parameters<T>): ReturnType<T> {
const key = keyOf(args);
if (cache.has(key)) {
return cache.get(key)!;
}
const result = method.apply(this, args) as ReturnType<T>;
cache.set(key, result);
return result;
} as T;
}
// 使用
class MathService {
@memoize
fib(n: number): number {
if (n < 2) return n;
return this.fib(n - 1) + this.fib(n - 2);
}
}
const svc = new MathService();
console.log(svc.fib(40)); // 第一次计算
console.log(svc.fib(40)); // 第二次命中缓存
10.1 学术论文
[1] Kiczales, G., Lamping, J., Mendhekar, A., Maeda, C., Lopes, C., Loingtier, J.-M., & Irwin, J. (1997). Aspect-oriented programming. In Proceedings of the 11th European Conference on Object-Oriented Programming (pp. 220–242). Springer. https://doi.org/10.1007/BFb0053381
[2] Bierman, G., Abadi, M., & Torgersen, M. (2014). Understanding TypeScript. In ECOOP 2014 – Object-Oriented Programming (pp. 257–281). Springer. https://doi.org/10.1007/978-3-662-44202-9_11
[3] Rastogi, A., Swamy, N., Fournet, C., Bierman, G., & Vekris, P. (2015). Safe & efficient gradual typing for TypeScript. In Proceedings of the 42nd Annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages (pp. 167–180). https://doi.org/10.1145/2676726.2676971
[4] Filman, R. E., Friedman, D. P., Aksit, M., & Clarke, S. (2005). Aspect-Oriented Software Development. Addison-Wesley.
10.2 官方规范
[5] ECMA TC39. (2023). Proposal: Decorators (Stage 3). https://github.com/tc39/proposal-decorators
[6] Microsoft. (2023). TypeScript 5.0 Release Notes: Decorators. https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/
[7] Microsoft. (2023). TypeScript 5.2 Release Notes: Symbol.metadata. https://devblogs.microsoft.com/typescript/announcing-typescript-5-2/
[8] ECMA International. (2024). ECMAScript 2024 Language Specification. https://tc39.es/ecma262/
10.3 标准提案
[9] Ehrenberg, D. (2022). Decorators proposal: Metadata. TC39 Meeting Notes. https://github.com/tc39/notes/blob/main/meetings/2022-03/mar-29.md
[10] Buckton, R. (2023). Proposal: Decorator Metadata (Stage 3). https://github.com/tc39/proposal-decorator-metadata
11.1 书籍
- Kiczales, G., et al. (1997). The AspectJ Programming Guide. Xerox PARC. — AOP 经典参考。
- Lopes, C. V. (2007). Aspect-Oriented Programming with usecases. Prentice Hall.
- Bracha, G. (2004). Executable Grammars in Java. — 元编程理论。
- Stefanov, S. (2023). TypeScript Design Patterns. O’Reilly. — 第 7 章 Decorator Pattern with TS 5.0。
11.3 相关源码
- TypeScript 编译器装饰器实现:
src/compiler/transformers/Decorators.ts - TypeScript 装饰器类型检查:
src/compiler/checker.ts中的checkDecorator - NestJS DI 核心:
packages/core/injector/ - TypeORM 装饰器:
src/decorator/ - TypeStack class-validator:
src/decorator/
11.4 进阶论文
-
Walker, R. J., Baniassad, E. L. A., & Murphy, G. C. (1999). An initial assessment of aspect-oriented programming. In Proceedings of the 21st International Conference on Software Engineering (pp. 120–130). https://doi.org/10.1145/302405.302413
-
Hannemann, J., & Kiczales, G. (2002). Design pattern implementation in Java and AspectJ. In Proceedings of the 17th ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications (pp. 161–173). https://doi.org/10.1145/582419.582436
-
Fraine, B. D., et al. (2014). Aspect-oriented programming in JavaScript: A study on decorators and mixins. Software: Practice and Experience, 44(10), 1185–1210.
附录 A:装饰器类型快速参考
| 装饰器类型 | TS 5.0+ context.kind | 实验性签名参数 | 用途 |
|---|---|---|---|
| 类装饰器 | 'class' | (target) | 修改类构造函数 |
| 方法装饰器 | 'method' | (target, key, descriptor) | 包装方法 |
| Getter/Setter | 'getter' / 'setter' | (target, key, descriptor) | 拦截访问 |
| 属性装饰器 | 'field' | (target, key) | 初始化/校验属性 |
| Accessor | 'accessor' | N/A(TS 5.0+ 新增) | 响应式状态 |
| 参数装饰器 | N/A | (target, key, index) | 注入/标记参数 |
附录 B:TS 5.0+ 装饰器 context 对象字段
| 字段 | 类型 | 含义 |
|---|---|---|
kind | 'class' | 'method' | ... | 装饰器类型 |
name | string | symbol | 成员名称 |
access | { has, get, set }(按 kind 部分提供) | 安全访问器(支持私有成员) |
static | boolean | 是否静态成员 |
metadata | Record<PropertyKey, unknown>(可变共享对象) | 类共享元数据对象,经 Symbol.metadata 挂载 |
addInitializer | (fn) => void | 注册初始化函数(构造后执行) |
附录 C:版本兼容性矩阵
| TS 版本 | 实验性装饰器 | TC39 Stage 3 | Symbol.metadata | addInitializer |
|---|---|---|---|---|
| 1.5 - 4.9 | 支持(experimentalDecorators) | 不支持 | 不支持 | 不支持 |
| 5.0+ | 支持(维护模式) | 支持 | 支持 | 支持 |
截至 2026-09,TC39 装饰器提案仍处于 Stage 3(未定稿);标准装饰器当前形态不含参数装饰器。
附录 D:常见错误代码
| 错误代码 | 含义 | 解决方案 |
|---|---|---|
| TS1271 | Decorator cannot be applied to a private field | 升级到 TS 5.0+ |
| TS1240 | Unable to resolve signature of class decorator | 检查装饰器返回类型 |
| TS1273 | Cannot use decorators in a .d.ts file | 将装饰器移到 .ts 文件 |
| TS1241 | Unable to resolve signature of method decorator | 使用 context-based 签名 |
| TS1329 | ‘experimentalDecorators’ is not enabled | 启用 experimentalDecorators 或升级到 TS 5.0+ |
附录 E:术语表
- Decorator:装饰器,修改类或类成员的高阶函数
- AOP:Aspect-Oriented Programming,面向切面编程
- Cross-cutting concern:横切关注点,如日志、事务、权限
- Weaving:织入,将切面应用到目标对象的过程
- Pointcut:切点,定义哪些连接点会被增强
- Advice:增强,切面在连接点处执行的动作
- Aspect:切面,模块化的横切关注点
- Metadata:元数据,描述数据的数据
- DI:Dependency Injection,依赖注入
- IoC:Inversion of Control,控制反转
附录 F:装饰器与传统 AOP 实现对比
| 维度 | TS 装饰器 | Spring AOP | AspectJ |
|---|---|---|---|
| 实现机制 | 函数包装 + 元数据 | 动态代理(CGLIB/JDK) | 字节码织入 |
| 性能 | 编译期一次 + 运行时包装 | 运行时代理开销 | 编译期织入,零运行时 |
| 表达力 | 类成员级 | 方法级 + 字段级 | 方法级 + 字段级 + 构造器级 |
| Pointcut | 显式标注 | 表达式(execution, within) | 表达式 + 通配符 |
| 运行时依赖 | 无(或 reflect-metadata) | Spring 容器 | AspectJ runtime |