TypeScript 5 装饰器完全指南:从旧语法迁移到 TC39 Stage 3 标准

深入解析 TypeScript 5 装饰器(TC39 Stage 3)新标准,对比旧 experimentalDecorators 语法,涵盖日志、验证、依赖注入等实战模式,附完整代码和迁移避坑指南。

前端开发 2026-06-02 16 分钟

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+ 支持。在旧浏览器中需要 polyfill Symbol.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 而非装饰器

📚 相关文章