首页 / TypeScript 入门教程 / 装饰器迁移与选型

TypeScript 入门教程

装饰器迁移与选型

本教程共 80 篇 · 第 71 篇 · 更新于 2026-08-10 · 约 12 分钟阅读

TypeScriptTypeScript 入门教程装饰器迁移Decorator Migration选型指南experimentalDecorators

本节目标:搞明白 legacy 装饰器和标准装饰器的实际取舍——能不能混用、怎么迁、新项目和老项目分别怎么选,以及避掉最常见的坑。

核心事实:不能混用

这是最重要的规则,放最前面说:

同一份文件(或者说同一个编译上下文)不能同时使用 legacy 和标准装饰器。

如果你在 tsconfig.json 里写了 "experimentalDecorators": true,那么所有 @ 语法都会被编译器按 legacy 规则解析。如果关掉这个选项,编译器就用标准装饰器规则。

两种规则下装饰器的函数签名完全不同:

// Legacy:一个参数
function legacyDecorator(constructor: Function) { }

// 标准:两个参数
function standardDecorator(value: Function, context: ClassDecoratorContext) { }

所以你把 legacy 写法的装饰器丢进标准模式,或者反过来——直接编译报错。

能不能在不同文件里用不同模式

技术上有两个办法实现”共存”:

方案一:拆成多个 tsconfig 项目。

project/
├── tsconfig.json          ← experimentalDecorators: false (新代码)
├── tsconfig.legacy.json   ← experimentalDecorators: true  (旧代码)
├── src/
│   ├── new-code/          ← 标准装饰器
│   └── legacy-code/       ← legacy 装饰器

用 TypeScript 的 project references 串联起来。但这在实际项目里维护成本很高——你要时刻记住哪个文件属于哪个编译上下文,依赖关系也会变复杂。

方案二:保持 legacy 模式,逐步迁移。

这是更现实的路径:所有代码继续用 experimentalDecorators: true,但你可以在自己能控制的装饰器代码里,逐步按标准装饰器的思路改写(即便运行在 legacy 模式)。等整条链路都改完了,再关掉 experimentalDecorators

不过对于 NestJS 或 Angular 项目,方案二不适用——框架的装饰器本身就是 legacy 的,你关不掉选项。

生态选型建议(2026 年版)

截止 2026 年 8 月,装饰器生态的现状如下:

继续用 legacy 的场景

Angular 项目:Angular 的 @Component@Input@Injectable 等核心装饰器重度依赖 legacy 的参数装饰器和 reflect-metadata。标准装饰器没有参数装饰器,Angular 团队的迁移路线图目前没有明确时间表。继续用,别折腾。

NestJS 项目:同理,NestJS 的 @Controller@Get@Param 等全部是 legacy 装饰器。虽然社区有讨论迁移方案(如利用 addInitializer 替代参数装饰器的部分功能),但官方尚未给出迁移路径。继续用,等官方支持。

TypeORM 项目@Entity@Column 等依赖 legacy 和 reflect-metadata 的元数据注入。TypeORM 团队目前没有公开的迁移计划。继续用。

class-validator / class-transformer 用户:同上,这两个库完全基于 legacy 装饰器和元数据反射。继续用。

可以尝试标准的场景

全新小项目、内部工具:如果你的项目不依赖上面提到的任何框架,装饰器使用量不大,从标准装饰器起步。不需要编译选项、类型更安全、API 更清晰。

自研框架或库:如果你在写一个给其他人用的工具库,建议同时提供 legacy 和标准两套装饰器入口——这在开源社区已经是比较常见的做法了。

纯前端项目(React / Vue):React 和 Vue 基本不用装饰器(Vue 的 vue-class-component 已经边缘化),所以不存在迁移压力。真要写装饰器的话,直接用标准的就行。

Note

TC39 装饰器提案自 2022 年进入 Stage 3 至今已有四年,社区预期它将在 2026-2027 年间推进到 Stage 4(正式成为 ECMAScript 标准的一部分)。一旦进入 Stage 4,浏览器引擎将原生支持装饰器,不再需要编译转换。

迁移策略:如果你一定要迁

假设你有一个纯自己维护的 legacy 装饰器代码库,想切换到标准模式。建议的步骤:

第一步:确认依赖项

检查所有第三方库,确认它们是否使用了 legacy 装饰器。特别是 node_modules 里那些用了 @Entity@Column 等装饰器的 ORM/校验库。

如果任何第三方库依赖 legacy 装饰器,迁移停止。等库更新。

第二步:逐文件替换

关掉 experimentalDecorators,看哪些文件报错。对于每个报错的装饰器:

类装饰器:从 (constructor) 改为 (value, context)

// Before (legacy)
function myDecorator<T extends new (...args: any[]) => any>(ctor: T) {
  return class extends ctor { /* ... */ };
}

// After (standard)
function myDecorator<T extends new (...args: any[]) => any>(
  value: T,
  context: ClassDecoratorContext
) {
  return class extends value { /* ... */ };
}

方法装饰器:不再操作 descriptor,直接返回新函数。

// Before (legacy)
function logger(target: any, key: string, desc: PropertyDescriptor) {
  const original = desc.value;
  desc.value = function (...args: any[]) {
    console.log(`调用 ${key}`);
    return original.apply(this, args);
  };
}

// After (standard)
function logger(value: Function, context: ClassMethodDecoratorContext) {
  const name = String(context.name);
  return function (this: any, ...args: any[]) {
    console.log(`调用 ${name}`);
    return value.apply(this, args);
  };
}

参数装饰器没有直接替代。 你需要重新设计这部分逻辑。常见的替代思路:

  • addInitializer 在类初始化时注入依赖。
  • 用属性装饰器 + accessor 替代参数注入。

第三步:处理 reflect-metadata

如果用了 Reflect.getMetadata("design:paramtypes", ...) 来做依赖注入,迁移到标准装饰器后,这部分逻辑需要重写——标准装饰器没有内置的元数据生成。

常见踩坑

坑一:没关 experimentalDecorators 就写标准语法

// tsconfig.json 里 experimentalDecorators: true
function logged(value: Function, context: ClassMethodDecoratorContext) {
  // ...
}
// ❌ 编译报错:装饰器签名不匹配

编译器会按 legacy 模式解析你的装饰器函数,发现参数数量不对,直接报错。

坑二:两个装饰器之间产生了命名冲突

// 文件 A (legacy)
export function log(target: any) { /* legacy 签名 */ }

// 文件 B (标准)
import { log } from "./A";
@log  // ❌ 如果当前文件是标准模式,log 的签名不对
class C {}

跨文件共享装饰器时,如果源文件和目标文件的编译模式不同,签名就不匹配。

坑三:以为 emitDecoratorMetadata 在标准模式也有效

emitDecoratorMetadata 只在 experimentalDecorators: true 时才会生成元数据。标准装饰器模式下,这个选项被忽略

坑四:标准装饰器的 addInitializer 执行时机

function initSomething(value: undefined, context: ClassFieldDecoratorContext) {
  context.addInitializer(function (this: any) {
    console.log(this.x); // ❌ undefined —— x 还没初始化
  });
}

class C {
  x = 42;
  @initSomething y = 0;
}

addInitializer 在构造函数执行期间运行,但早于属性初始化。所以不能依赖其他实例属性的值。

坑五:标准装饰器里 this 的指向

function log(value: Function, context: ClassMethodDecoratorContext) {
  return function (...args: any[]) {
    console.log(this); // this 是实例,不需要手动 bind
    return value.apply(this, args);
  };
}

标准装饰器返回的函数里的 this 自动指向实例——和 legacy 不同,你不需要在 apply 里手动传 this。但还是建议保留 value.apply(this, args) 的写法,保证显式可控。

一个决策流程图

帮你快速判断自己该用哪个:

你的项目用了 Angular / NestJS / TypeORM?
├── 是 → legacy,别动
└── 否 → 用了 class-validator / class-transformer?
    ├── 是 → legacy,等库更新
    └── 否 → 是 2026 年新开的项目?
        ├── 是 → 用标准装饰器
        └── 否 → 现有 legacy 项目?
            ├── 装饰器数量少(<20 个)→ 考虑迁移到标准
            └── 装饰器数量多 → 保持 legacy,新功能用标准但要拆分 tsconfig

写在最后

装饰器是 TypeScript 里最有争议、也最有魅力的特性之一。十年的时间里它走过了从”实验性语法糖”到”国际标准候选”的漫漫长路。

对于学习者来说:两种都要学。legacy 装饰器让你能读懂 Angular 和 NestJS 的源码,标准装饰器让你能跟上语言的演进方向。

对于实际项目来说:不用急。legacy 装饰器没有被遗弃,标准装饰器的生态还需要时间成长。选最适合你当前项目的那一套,保持关注,适时而动。