TS装饰器详解

在 TypeScript 5.0 中,TS 正式支持了 TC39 Stage 3 标准装饰器(Standard Decorators) 。这是 ECMAScript 规范原生的装饰器方案,代表了 JS/TS 装饰器的未来方向。

与旧版(Legacy/Stage 1)相比,标准装饰器不再依赖 experimentalDecorators 配置 experimentalDecorators,彻底解决了与 JS 原生私有字段(#field)不兼容的问题,并且引入了类型安全且不依赖额外包的元数据机制。

一、核心机制:统一签名与 Context 对象

TS 5.0+ 标准装饰器的基础形式非常简单,所有装饰器函数都接收两个参数:

arduino 复制代码
function myDecorator(value: any, context: DecoratorContext) {
  // value: 被装饰的目标(类、方法、访问器等)
  // context: 上下文对象,包含目标的元数据与控制函数
}

context 上下文对象包含的字段

属性名 类型 说明
kind string 目标类型:'class' /'method'/'getter' / 'setter' / 'field' / 'accessor'
name string/symbol 被装饰的成员名称或类名
static boolean 是否为静态成员(类装饰器时为 undefined
private boolean 是否为原生私有成员(如 #name
access object 包含 get / set 方法,用于在运行时动态读取或修改成员
addInitializer function 注册一个初始化回调函数(在实例创建或类声明时触发)
metadata object 挂载在类上的元数据字典(基于 Symbol.metadata

二、5 大类标准装饰器详解与类型定义

TS 为每一种装饰器都内置了严格的泛型上下文类型(如 ClassMethodDecoratorContext)。

1. 方法装饰器 (Method Decorators)

  • 签名(value: TargetFunction, context: ClassMethodDecoratorContext) => ReplacementFunction | void
  • 作用:拦截方法调用、记录日志、防抖/节流、异常捕捉。
typescript 复制代码
function LogExecution<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
  const methodName = String(context.name);

  // 返回一个新的函数来替换原方法
  return function (this: This, ...args: Args): Return {
    console.log(`[LOG] Calling ${methodName} with args:`, args);
    const result = target.call(this, ...args);
    console.log(`[LOG] ${methodName} returned:`, result);
    return result;
  };
}

class Calculator {
  @LogExecution
  add(a: number, b: number) {
    return a + b;
  }
}

const calc = new Calculator();
calc.add(2, 3); // 自动打印调用日志和返回值

2. 类装饰器 (Class Decorators)

  • 签名(value: Constructor, context: ClassDecoratorContext) => ReplacementConstructor | void
  • 作用:扩展类、混入(Mixin)新功能、注册全局类服务。
typescript 复制代码
function Timestamped<T extends abstract new (...args: any[]) => any>(
  target: T,
  context: ClassDecoratorContext<T>
) {
  // 返回继承自原类的子类
  return class extends target {
    createdAt = new Date();
  };
}

@Timestamped
class User {
  constructor(public name: string) {}
}

const user = new User("Alice");
console.log((user as any).createdAt); // 访问装饰器注入的属性

3. 属性/字段装饰器 (Field Decorators)

  • 关键区别 :字段在类定义时尚未赋值 ,因此第一个参数 value 恒为 undefined
  • 签名(value: undefined, context: ClassFieldDecoratorContext) => InitializerFunction | void
  • 返回值 :返回一个接收初始值并返回新值的函数 (initialValue: T) => T
javascript 复制代码
function DefaultValue<T>(defaultValue: T) {
  return function (value: undefined, context: ClassFieldDecoratorContext) {
    // 返回初始化回调函数
    return function (initialValue: T) {
      return initialValue ?? defaultValue;
    };
  };
}

class Profile {
  @DefaultValue("Guest")
  username?: string;
}

const p1 = new Profile();
console.log(p1.username); // "Guest"

4. 自动访问器装饰器 (Auto-Accessor Decorators)

TS 5.0 配合 ES 标准引入了全新的 accessoraccessor 语法。accessor 会自动生成私有存储字段以及对应的 getter 和 setter。

  • 签名(value: { get: () => T, set: (v: T) => void }, context: ClassAccessorDecoratorContext) => { get?, set?, init? } | void
  • 作用:同时拦截读取和写入,是实现响应式系统(如 Vue Reactivity / Signals)的利器。
typescript 复制代码
function MinMax(min: number, max: number) {
  return function <T, V extends number>(
    target: ClassAccessorDecoratorTarget<T, V>,
    context: ClassAccessorDecoratorContext<T, V>
  ): ClassAccessorDecoratorResult<T, V> {
    return {
      set(value: V) {
        if (value < min || value > max) {
          throw new Error(`属性 ${String(context.name)} 必须在 ${min} 和 ${max} 之间!`);
        }
        target.set.call(this, value);
      }
    };
  };
}

class Player {
  // 使用 accessor 关键字声明自动访问器
  @MinMax(0, 100)
  accessor health = 100;
}

const p = new Player();
p.health = 50;  // 正常
// p.health = 150; // 报错: 属性 health 必须在 0 和 100 之间!

5. Accessor (Getter / Setter) 装饰器

如果你显式编写了 getset 方法,可以分别进行拦截。

typescript 复制代码
function Precise(decimalPlaces: number) {
  return function <This, Return extends number>(
    target: (this: This) => Return,
    context: ClassGetterDecoratorContext<This, Return>
  ) {
    return function (this: This): Return {
      const result = target.call(this);
      return Number(result.toFixed(decimalPlaces)) as Return;
    };
  };
}

class Circle {
  constructor(public radius: number) {}

  @Precise(2)
  get area() {
    return Math.PI * this.radius * this.radius;
  }
}

const c = new Circle(5);
console.log(c.area); // 78.54

三、高级核心特性

1. 使用 addInitializer 实现自动绑定 (@bound)

在过去的 TS 中实现 this 绑定通常要修改描述符,而在标准装饰器中,官方提供了 context.addInitializer 方法。它会在**实例构造完成时(构造函数末尾)**自动调用:

typescript 复制代码
function bound<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
  const methodName = context.name;

  // 注册初始化函数
  context.addInitializer(function (this: any) {
    // this 指向正在创建的实例
    this[methodName] = this[methodName].bind(this);
  });
}

class Button {
  constructor(private label: string) {}

  @bound
  onClick() {
    console.log(`Clicked: ${this.label}`);
  }
}

const btn = new Button("Submit");
const clickHandler = btn.onClick;
clickHandler(); // 输出 "Clicked: Submit",this 不会丢失!

2. 支持原生私有字段 (#field)

旧版装饰器由于使用字符串 key 操作 prototype,无法作用于 JS 原生私有成员。TS 5.0+ 标准装饰器完美支持 # 私有成员:

typescript 复制代码
function WatchPrivate(target: any, context: ClassMethodDecoratorContext) {
  console.log(`私有方法 ${String(context.name)} 已成功装饰,private=${context.private}`); // private = true
}

class SecretService {
  #internalKey = "123456";

  @WatchPrivate
  #getSecret() {
    return this.#internalKey;
  }
}

3. 标准元数据支持 (context.metadata)

标准装饰器废弃了 reflect-metadata 第三方库,引入了标准的 context.metadata 对象。该对象会在同类所有装饰器共享 ,最终挂载到 Symbol.metadata 上。

less 复制代码
function Tag(tagName: string) {
  return function (target: any, context: ClassMethodDecoratorContext) {
    // 给 context.metadata 写入元数据
    context.metadata[context.name] = tagName;
  };
}

class ApiController {
  @Tag("GET_USERS")
  getUsers() {}

  @Tag("POST_USER")
  createUser() {}
}

// 读取类上的原生元数据
const metadata = ApiController[Symbol.metadata];
console.log(metadata); 
// 输出: { getUsers: 'GET_USERS', createUser: 'POST_USER' }

四、执行顺序全解

当在一个类中混合使用多种装饰器时,其执行生命周期分为两个阶段:

  1. 求值与注入阶段 (Evaluation & Decorator Application)

    • 类定义时按照以下顺序评估并施加装饰器:

      1. 实例成员(方法、访问器、字段)
      2. 静态成员(方法、访问器、字段)
      3. 类装饰器
    • 同一个目标 挂载多个装饰器时,按洋葱模型(自下而上 / 由内而外)执行。

  2. 初始化回调阶段 (Initializers Execution)

    • new Class() 被调用时,按照字段定义顺序依次触发由 addInitializer 注册的回调。

五、标准 vs 旧版 关键差异总结表

对比维度 Standard 标准装饰器 (TS 5.0+) Legacy 实验性装饰器 (experimentalDecorators)
标准状态 TC39 Stage 3 (官方标准) TC39 Stage 1 (已废弃/过时)
参数装饰器 暂不支持 (Stage 3 暂未包含) ✅ 支持 (如 NestJS @Param())
原生理私有成员 ✅ 完美支持 (#private) ❌ 无法支持
类型安全 高(强制使用泛型约束 This 类型) 一般
thisthis 绑定 依赖 context.addInitializer 修改 PropertyDescriptor.get
元数据存储 借助内置 context.metadata 依赖 reflect-metadata
相关推荐
晓说前端21 小时前
TypeScript 核心语法进阶 —— 字面量类型与类型推论
前端·typescript
布兰妮甜1 天前
从 0 搭建企业级 Vue3/Vite 脚手架(规范、eslint、husky、打包、环境变量全流程)
typescript·vue3·vite·脚手架·前端工程化
东方小月1 天前
从0开发一个 Coding Agent(一):前言
前端·人工智能·typescript
gis开发之家2 天前
《Vue3 从入门到大神35篇》Vue3 源码详解(五):effect 依赖收集原理——track 与 trigger 是如何工作的?
javascript·typescript·前端框架·vue3·vue3源码
退休倒计时2 天前
【每日一题】LeetCode 131. 分割回文串 TypeScript
算法·leetcode·typescript
橘子星3 天前
在浏览器里跑大模型!用 WebGPU 零成本部署 DeepSeek-R1
前端·typescript
bonechips3 天前
React + WebGPU:在浏览器里跑一个 DeepSeek 推理模型
react.js·typescript
触底反弹3 天前
🔥 React 零基础入门(上):环境搭建 + JSX 深度解析
前端·react.js·typescript
谷哥的小弟4 天前
TypeScript对象类型
javascript·typescript