2023 年 TypeScript 5.0 正式支持 TC39 Stage 3 装饰器提案,标志着 JavaScript 元编程进入标准化时代。根据 State of JS 2025 调查,62% 的 TypeScript 开发者在项目中使用装饰器,但其中超过一半仍在使用已废弃的 experimentalDecorators 语法。如果你还在用 @observable、@computed 那套旧语法,这篇文章会帮你彻底理清新旧装饰器的差异,并掌握生产级的实战模式。
🔐 一、新旧装饰器核心差异
为什么需要迁移?
旧版装饰器(experimentalDecorators)基于一个从未进入 TC39 标准的早期提案,TypeScript 从 1.5 版本开始支持。它的设计存在几个根本性问题:元数据依赖 reflect-metadata polyfill、装饰器执行顺序不一致、与原生 JavaScript 类字段初始化存在冲突。
新版装饰器(TC39 Stage 3)在 TypeScript 5.0+ 中实现,采用全新的 DecoratorContext 对象体系,不再需要 reflect-metadata,执行顺序明确定义,并且最终会成为 ECMAScript 标准的一部分。
📌 **记住:**旧版
experimentalDecorators永远不会成为 JavaScript 标准。所有新项目应该直接使用新版装饰器。
装饰器种类对比
新版标准定义了五种装饰器类型,每种对应不同的类成员:
| 装饰器类型 | 目标 | 旧版等价 | 推荐使用 |
|---|---|---|---|
| Class Decorator | 类本身 | @Component |
✅ 推荐 |
| Method Decorator | 类方法 | @Action |
✅ 推荐 |
| Getter/Setter Decorator | 存取器 | @computed |
✅ 推荐 |
| Field Decorator | 类字段 | ❌ 不存在 | ✅ 新增 |
| Auto-accessor Decorator | 自动存取器 | ❌ 不存在 | ✅ 新增 |
⚠️ **警告:**新版 Field Decorator 与旧版 Field Decorator 行为完全不同。旧版中
@decorator加在字段上实际上是 Method Decorator;新版中它有自己的上下文对象和初始化逻辑。
DecoratorContext 对象详解
新版装饰器的核心是 DecoratorContext 对象,它提供了标准化的元编程能力:
// DecoratorContext 类型定义(简化版)
interface DecoratorContext {
kind: 'class' | 'method' | 'getter' | 'setter' | 'field' | 'accessor';
name: string | symbol;
static: boolean;
private: boolean;
access: {
get?(this: object): unknown;
set?(this: object, value: unknown): void;
has?(this: object): boolean;
};
metadata: object; // 标准元数据对象,无需 polyfill
addInitializer(initializer: () => void): void; // 注册初始化回调
}
// ❌ 旧版:需要 reflect-metadata polyfill
import 'reflect-metadata';
function Log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${key} with`, args);
return original.apply(this, args);
};
// 元数据操作依赖外部 polyfill
Reflect.defineMetadata('logged', true, target, key);
}
// ✅ 新版:原生 DecoratorContext,无需 polyfill
function log(value: Function, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
context.metadata[methodName + '_logged'] = true;
return function (this: any, ...args: any[]) {
console.log(`Calling ${methodName} with`, args);
return value.apply(this, args);
};
}
⚡ **关键结论:**新版装饰器的第一个参数是被装饰的值本身(不是 target),第二个参数是
DecoratorContext。这与旧版的(target, key, descriptor)签名完全不同,迁移时需要逐个调整。
🚀 二、五大实战模式
模式一:方法日志装饰器
这是最常见的装饰器用法。新版装饰器返回一个替换函数,替代原方法:
// 方法日志装饰器 — 记录方法调用、参数和返回值
function logged(value: Function, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
return function (this: any, ...args: unknown[]) {
const start = performance.now();
console.log(`📝 [${methodName}] 调用开始,参数:`, JSON.stringify(args));
const result = value.apply(this, args);
// 支持异步方法
if (result instanceof Promise) {
return result.then((resolved) => {
const duration = (performance.now() - start).toFixed(2);
console.log(`✅ [${methodName}] 完成 (${duration}ms),返回:`, resolved);
return resolved;
});
}
const duration = (performance.now() - start).toFixed(2);
console.log(`✅ [${methodName}] 完成 (${duration}ms),返回:`, result);
return result;
};
}
// 使用示例
class UserService {
@logged
async getUserById(id: number) {
// 模拟 API 请求
await new Promise(r => setTimeout(r, 100));
return { id, name: '张三', email: 'zhangsan@example.com' };
}
@logged
formatName(first: string, last: string) {
return `${last}${first}`;
}
}
const service = new UserService();
service.formatName('三', '张');
// 📝 [formatName] 调用开始,参数: ["三","张"]
// ✅ [formatName] 完成 (0.12ms),返回: 张三
模式二:参数验证装饰器
结合新版 Field Decorator 和 Auto-accessor,实现声明式参数验证:
// 参数验证装饰器 — 在方法调用前验证参数
function validate(
...rules: Array<(args: unknown[]) => string | null>
) {
return function validateDecorator(
value: Function,
context: ClassMethodDecoratorContext
) {
return function (this: any, ...args: unknown[]) {
for (const rule of rules) {
const error = rule(args);
if (error) {
throw new TypeError(`参数验证失败: ${error}`);
}
}
return value.apply(this, args);
};
};
}
// 验证规则工厂函数
function required(index: number) {
return (args: unknown[]) =>
args[index] == null ? `第 ${index + 1} 个参数不能为空` : null;
}
function isNumber(index: number) {
return (args: unknown[]) =>
typeof args[index] !== 'number' ? `第 ${index + 1} 个参数必须是数字` : null;
}
function minLength(index: number, min: number) {
return (args: unknown[]) => {
const val = args[index];
return typeof val === 'string' && val.length < min
? `第 ${index + 1} 个参数长度不能少于 ${min}`
: null;
};
}
class OrderService {
@validate(required(0), isNumber(0))
createOrder(productId: number) {
return { orderId: Date.now(), productId };
}
@validate(required(0), minLength(0, 2), required(1), isNumber(1))
createFullOrder(customerName: string, amount: number) {
return { orderId: Date.now(), customerName, amount };
}
}
const orders = new OrderService();
orders.createOrder(42); // ✅ 正常
orders.createOrder('invalid' as any); // ❌ TypeError: 参数验证失败: 第 1 个参数必须是数字
orders.createFullOrder('张', 100); // ❌ TypeError: 参数验证失败: 第 1 个参数长度不能少于 2
模式三:Auto-accessor 状态管理
Auto-accessor 是新版装饰器引入的全新概念,它将普通字段转换为 getter/setter 对,非常适合状态管理:
// Auto-accessor 装饰器 — 响应式状态管理
function observable<This, V>(
target: ClassAccessorDecoratorTarget<This, V>,
context: ClassAccessorDecoratorContext<This>
) {
const subscribers = new Map<object, Set<() => void>>();
const fieldName = String(context.name);
return {
get(this: This) {
return target.get.call(this);
},
set(this: This, newValue: V) {
const oldValue = target.get.call(this);
if (oldValue === newValue) return;
target.set.call(this, newValue);
console.log(`🔄 [${fieldName}] ${oldValue} → ${newValue}`);
// 触发订阅者
const subs = subscribers.get(this);
if (subs) {
subs.forEach(fn => fn());
}
},
} as ClassAccessorDecoratorResult<This, V>;
}
// 自动计算装饰器 — 依赖变化时自动重算
function computed<T>(
getter: (instance: T) => any
) {
return function computedDecorator(
_: undefined,
context: ClassFieldDecoratorContext<T>
) {
const fieldName = String(context.name);
let cached: any;
let dirty = true;
// 使用 addInitializer 在实例初始化后设置 getter
context.addInitializer(function () {
const instance = this;
Object.defineProperty(instance, fieldName, {
get() {
if (dirty) {
cached = getter(instance);
dirty = false;
}
return cached;
},
enumerable: true,
configurable: true,
});
});
};
}
class CartStore {
@observable
accessor items: Array<{ name: string; price: number }> = [];
@observable
accessor discount: number = 0;
get totalPrice() {
const subtotal = this.items.reduce((sum, item) => sum + item.price, 0);
return subtotal * (1 - this.discount);
}
}
const cart = new CartStore();
cart.items = [{ name: '键盘', price: 299 }, { name: '鼠标', price: 99 }];
// 🔄 [items] [] → [{name: "键盘", price: 299}, {name: "鼠标", price: 99}]
console.log(cart.totalPrice); // 398
cart.discount = 0.1;
// 🔄 [discount] 0 → 0.1
console.log(cart.totalPrice); // 358.2
模式四:Class Decorator 依赖注入
Class Decorator 在新版中保持了强大的类级别修改能力,非常适合实现轻量级依赖注入:
// 依赖注入容器
class Container {
private static services = new Map<string, any>();
static register<T>(token: string, instance: T) {
this.services.set(token, instance);
}
static resolve<T>(token: string): T {
const service = this.services.get(token);
if (!service) throw new Error(`Service ${token} not registered`);
return service as T;
}
}
// Class Decorator — 自动注册到容器
function service<T extends new (...args: any[]) => any>(
token: string
) {
return function serviceDecorator(
target: T,
context: ClassDecoratorContext<T>
) {
context.addInitializer(function () {
Container.register(token, new target());
});
return target;
};
}
// Method Decorator — 自动注入依赖
function inject(token: string) {
return function injectDecorator(
value: Function,
context: ClassMethodDecoratorContext
) {
return function (this: any, ...args: unknown[]) {
const dependency = Container.resolve(token);
return value.call(this, dependency, ...args);
};
};
}
// Logger 服务
@service('logger')
class Logger {
log(message: string) {
console.log(`[${new Date().toISOString()}] ${message}`);
}
}
// 使用注入
class UserController {
@inject('logger')
createUser(logger: Logger, name: string) {
logger.log(`Creating user: ${name}`);
return { id: Date.now(), name };
}
}
const controller = new UserController();
controller.createUser('李四');
// [2026-06-03T12:00:00.000Z] Creating user: 李四
模式五:元数据与反射
新版装饰器内置 context.metadata 对象,不再需要 reflect-metadata polyfill。这使得框架可以在运行时读取装饰器附加的元信息:
// 定义元数据 Schema 装饰器
function schema(schemaDef: Record<string, string>) {
return function schemaDecorator(
value: undefined,
context: ClassDecoratorContext
) {
// 直接使用 context.metadata,无需 reflect-metadata
context.metadata.schema = schemaDef;
context.metadata.createdAt = Date.now();
};
}
// 标记必填字段
function field(description: string) {
return function fieldDecorator(
value: undefined,
context: ClassFieldDecoratorContext
) {
const fields = (context.metadata.fields ??= []) as Array<{
name: string;
description: string;
}>;
fields.push({
name: String(context.name),
description,
});
};
}
@schema({ version: '1.0', type: 'user' })
class UserProfile {
@field('用户姓名,2-20 个字符')
name: string = '';
@field('用户邮箱,必须符合邮箱格式')
email: string = '';
@field('用户年龄,0-150')
age: number = 0;
}
// 读取元数据 — 框架层面使用
const metadata = UserProfile[Symbol.metadata];
console.log(metadata?.schema);
// { version: "1.0", type: "user" }
console.log(metadata?.fields);
// [{ name: "name", description: "..." }, ...]
💡 提示:
Symbol.metadata是 TC39 标准的一部分,TypeScript 5.2+ 支持。在旧浏览器中需要 polyfillSymbol.metadata。
💡 三、迁移策略与避坑指南
从 experimentalDecorators 迁移的步骤
迁移不是简单的搜索替换。新版装饰器的签名、返回值、上下文对象都不同,需要系统性地调整:
第一步:更新 tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": false,
"emitDecoratorMetadata": false,
"lib": ["ES2022", "DOM"]
}
}
⚠️ **警告:**移除
experimentalDecorators: true后,所有使用旧语法的装饰器会立即报错。建议在独立分支中进行迁移。
第二步:逐个替换装饰器签名
旧版签名与新版签名的对应关系:
// ❌ 旧版签名
// Method: (target, key, descriptor) => void
// Class: (target) => void
// Field: (target, key) => void (实际是 Method Decorator)
// Getter: (target, key, descriptor) => void
// ✅ 新版签名
// Method: (value: Function, context: MethodContext) => Function | void
// Class: (value: Function, context: ClassContext) => Function | void
// Field: (value: undefined, context: FieldContext) => (initialValue: unknown) => unknown | void
// Auto-accessor: (target: AccessorTarget, context: AccessorContext) => AccessorResult
第三步:处理元数据依赖
如果旧项目使用了 Reflect.getMetadata / Reflect.defineMetadata,需要用 context.metadata 替代。对于框架级别的元数据(如 TypeORM、NestJS),需要等待框架发布支持新版装饰器的版本。
常见陷阱
❌ 陷阱一:装饰器执行顺序不同
// 新版装饰器的执行顺序:先外后内(与旧版相反)
@outer // 第一个执行
@inner // 第二个执行
class MyClass {}
// 如果你的代码依赖旧版的执行顺序,迁移后行为会改变
❌ 陷阱二:Field Decorator 不能修改属性描述符
// 新版 Field Decorator 返回的是初始值转换函数,不是 descriptor
function uppercase(value: undefined, context: ClassFieldDecoratorContext) {
return function (initialValue: string) {
return initialValue.toUpperCase(); // 返回新的初始值
};
}
class User {
@uppercase
name: string = 'hello'; // 实际存储 'HELLO'
}
❌ 陷阱三:Auto-accessor 不适用于所有场景
// Auto-accessor 会将字段从普通属性变为 getter/setter
// 这会影响 Object.keys()、JSON.stringify() 等行为
class Config {
@observable
accessor apiUrl: string = 'https://api.example.com';
}
const config = new Config();
// JSON.stringify(config) — apiUrl 不会出现在输出中
// 因为它是 accessor(getter/setter),不是自有属性
框架支持状态
| 框架/库 | 旧版支持 | 新版支持 | 备注 |
|---|---|---|---|
| NestJS | ✅ 完整 | 🔶 10.x 实验性 | 需要启用 decoratorOptions |
| TypeORM | ✅ 完整 | ❌ 未支持 | 等待 0.4.x |
| MobX | ✅ 完整 | ✅ 6.x | makeObservable 已适配 |
| Angular | ✅ 完整 | ✅ 17+ | 默认使用新版 |
Vue 3 + vue-class-component |
✅ 完整 | 🔶 社区方案 | 推荐使用 Composition API |
| Zod | N/A | ✅ 原生 | 无需装饰器 |
💡 **提示:**如果你的项目重度依赖 TypeORM 或 NestJS,短期内建议保持
experimentalDecorators: true,等框架官方支持后再迁移。
✅ 总结与建议
TypeScript 5 装饰器是 JavaScript 元编程的未来标准,但迁移不是一蹴而就的事。对于不同场景,我的建议是:
**新项目(2026 年启动):**直接使用新版装饰器,不要开启 experimentalDecorators。如果需要 ORM,优先选择 Prisma 或 Drizzle(不依赖装饰器)。
**存量项目(使用旧装饰器):**评估框架依赖。如果主要依赖 NestJS/TypeORM,等框架官方支持后再迁移。如果是自定义装饰器,可以立即开始逐个替换。
**库/框架开发者:**尽快适配新版装饰器,同时提供旧版兼容路径。使用 Symbol.metadata 替代 reflect-metadata。
// ✅ 推荐:新项目装饰器模板
function createDecorator<TArgs extends unknown[], TReturn>(
fn: (
value: (...args: TArgs) => TReturn,
context: ClassMethodDecoratorContext,
...args: unknown[]
) => ((...args: TArgs) => TReturn) | void
) {
return function (...extraArgs: unknown[]) {
return function (
value: (...args: TArgs) => TReturn,
context: ClassMethodDecoratorContext
) {
return fn(value, context, ...extraArgs);
};
};
}
// 使用工厂创建参数化装饰器
const throttle = createDecorator<[number], void>((value, context, ms) => {
let lastCall = 0;
return function (this: any, ...args) {
const now = Date.now();
if (now - lastCall >= (ms as number)) {
lastCall = now;
value.apply(this, args);
}
};
});
class SearchService {
@throttle(300)
onSearch(query: string) {
console.log(`Searching: ${query}`);
}
}
相关工具推荐:
- TypeScript Playground:在线测试新版装饰器语法
- ts-morph:TypeScript AST 操作库,可用于批量迁移脚本
- Effect-TS:不依赖装饰器的函数式 Effect 系统,适合新项目
- Drizzle ORM:类型安全 ORM,使用 schema-first 而非装饰器