首页 / TypeScript 入门教程 / TC39 Stage 3 标准装饰器

TypeScript 入门教程

TC39 Stage 3 标准装饰器

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

TypeScriptTypeScript 入门教程标准装饰器Stage 3 DecoratorTC39auto-accessorcontext API

本节目标:掌握 TC39 Stage 3 标准装饰器的全新设计——统一的 (value, context) 签名、上下文对象 API、auto-accessor 关键字,以及六种装饰器类型的具体用法。理解它与 legacy 的本质区别。

TypeScript 5.0 起,标准装饰器不需要任何编译选项,直接写就行。你只需要确保 tsconfig.jsontarget 在 ES2015 以上(TS 7.0 的默认 target 就是 ES2015+),因为 auto-accessor 需要 class fields 支持。

全新的签名:(value, context)

标准装饰器抛弃了 legacy 那种”不同类型不同参数签名”的设计,统一成两个参数

type Decorator = (
  value: DecoratedValue,
  context: DecoratorContext
) => void | ReplacementValue;
  • value:被装饰的东西。类就是类本身,方法是方法函数,属性是 undefined
  • context:上下文对象,里面包含了元信息钩子方法

context 对象是所有装饰器共用的设计,不同装饰器类型里的字段会略有不同。

context 对象的通用字段

字段类型说明
kindstring装饰器类型:"class" / "method" / "getter" / "setter" / "field" / "accessor"
namestring | symbol被装饰的成员名称(类装饰器时为 undefined
staticboolean是否是静态成员
privateboolean是否是私有成员(# 打头)
addInitializer(fn)(fn: () => void) => void注册初始化回调,在类构造时执行

注意 kind 的可取值共 6 种,意味着标准装饰器有 6 种类型——比 legacy 多了一种 accessor(对应 auto-accessor 关键字),少了一种参数装饰器。

类装饰器

类装饰器的 value 就是类本身(构造函数),context.kind"class"

function sealed(value: Function, context: ClassDecoratorContext) {
  if (context.kind === "class") {
    Object.seal(value);
    Object.seal(value.prototype);
  }
}

@sealed
class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

返回值:返回新类替换原类

function countInstances<T extends new (...args: any[]) => any>(
  value: T,
  context: ClassDecoratorContext
) {
  let count = 0;

  return class extends value {
    constructor(...args: any[]) {
      super(...args);
      (this as any).instanceId = ++count;
    }
  } as any;
}

@countInstances
class Product {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

const a = new Product("A");
const b = new Product("B");
console.log((a as any).instanceId); // 1
console.log((b as any).instanceId); // 2

addInitializer:类初始化钩子

context.addInitializer() 注册一个函数,在类定义完全结束后执行:

function register(name: string) {
  return function (value: Function, context: ClassDecoratorContext) {
    context.addInitializer(() => {
      console.log(`组件 ${name} 注册完成`);
    });
  };
}

@register("UserCard")
class UserCard {}
// 输出:组件 UserCard 注册完成

方法装饰器

方法装饰器的 value 就是方法函数本身:

function log(
  value: Function,
  context: ClassMethodDecoratorContext
) {
  if (context.kind === "method") {
    const methodName = String(context.name);
    return function (this: any, ...args: any[]) {
      console.log(`调用 ${methodName}(${args.join(", ")})`);
      return value.apply(this, args);
    };
  }
}

class Calculator {
  @log
  add(x: number, y: number) {
    return x + y;
  }
}

const calc = new Calculator();
calc.add(3, 5);
// 输出:调用 add(3, 5)

跟 legacy 的关键区别:没有 descriptor。你直接返回一个新函数替换原方法,不需要修改描述对象。更干净、更符合直觉。

addInitializer:替代构造函数里的 bind

以前你必须在 constructor 里写 this.method = this.method.bind(this),现在可以移到装饰器里:

function bound(
  value: Function,
  context: ClassMethodDecoratorContext
) {
  if (context.private) {
    throw new Error("不能绑定私有方法");
  }
  const methodName = context.name;
  context.addInitializer(function (this: any) {
    this[methodName] = this[methodName].bind(this);
  });
}

addInitializer 的 this 指向类的实例,执行时机在构造函数里、早于属性初始化。

属性(field)装饰器

这是标准装饰器对 legacy 的一个重大改进。legacy 的属性装饰器拿不到值,标准装饰器虽然 value 也是 undefined,但可以返回一个初始化函数

function double(
  value: undefined,
  context: ClassFieldDecoratorContext
) {
  if (context.kind === "field") {
    return function (initialValue: number) {
      return initialValue * 2;
    };
  }
}

class C {
  @double
  x = 10;
}

const c = new C();
console.log(c.x); // 20

返回的初始化函数接收属性初始值,返回最终值。这解决了 legacy 属性装饰器”既不能读也不能写”的尴尬。

还可以通过 context.access 取出属性的存取器,在类外部读写:

let accessor: any;

function expose(
  value: undefined,
  context: ClassFieldDecoratorContext
) {
  accessor = context.access;
}

class C {
  @expose
  name = "Alice";
}

const c = new C();
console.log(accessor.get(c)); // "Alice"
accessor.set(c, "Bob");
console.log(c.name); // "Bob"

getter / setter 装饰器

getter 装饰器的 context.kind"getter",setter 是 "setter"。它们各自独立,允许分别装饰:

function validate(
  value: Function,
  context: ClassSetterDecoratorContext
) {
  if (context.kind === "setter") {
    return function (this: any, newVal: number) {
      if (newVal > 100) {
        throw new Error("值不能超过 100");
      }
      return value.call(this, newVal);
    };
  }
}

class C {
  #score: number = 0;

  @validate
  set score(v: number) {
    this.#score = v;
  }

  get score() {
    return this.#score;
  }
}

跟 legacy 不同,标准装饰器的 getter 和 setter 可以分别装饰——因为两个装饰器各自独立,不存在冲突。

auto-accessor:标准装饰器的独门武器

accessor 关键字是标准装饰器带来的全新语法。它把一个类属性变成自带 getter/setter 的”自动存取器”

class C {
  accessor x = 1;
}

// 等价于:
class C {
  #x = 1;

  get x() { return this.#x; }
  set x(val) { this.#x = val; }
}

accessor 本身没啥特殊的,但它可以和装饰器配合,产生强大的组合效果:

function logged(
  value: { get: () => unknown; set: (v: unknown) => void },
  context: ClassAccessorDecoratorContext
) {
  if (context.kind === "accessor") {
    const { get, set } = value;
    return {
      get() {
        const result = get.call(this);
        console.log(`读取 ${String(context.name)} → ${result}`);
        return result;
      },
      set(newVal) {
        console.log(`设置 ${String(context.name)} ← ${newVal}`);
        return set.call(this, newVal);
      },
      init(initialValue: unknown) {
        console.log(`初始化 ${String(context.name)} = ${initialValue}`);
        return initialValue;
      },
    };
  }
}

class C {
  @logged accessor x = 1;
}

const c = new C();
// 初始化 x = 1
c.x;
// 读取 x → 1
c.x = 42;
// 设置 x ← 42

accessor 装饰器返回的对象有三个可选方法:

  • get:拦截读取。
  • set:拦截写入。
  • init:修改初始值。
Tip

accessor 也可以跟 static# 一起用:static accessor x = 1accessor #y = 2

执行顺序

标准装饰器的执行顺序和 legacy 类似,但语义上更清晰:

  1. 评估阶段:先计算所有 @ 后面表达式的值(从上到下)。
  2. 应用阶段:先应用实例成员装饰器(方法 → getter → setter → field → accessor),然后是静态成员装饰器,最后是类装饰器。

同一成员上多个装饰器仍然是从下往上执行(后写的先执行)。

function d(name: string): any {
  console.log(`评估 ${name}`);
  return function () {
    console.log(`应用 ${name}`);
  };
}

@d("类")
class T {
  @d("方法")
  m() {}
}

// 评估 类
// 评估 方法
// 应用 方法
// 应用 类

六种标准装饰器速查

类型context.kindvalue返回值
"class"类本身Function(替换类)
方法"method"方法函数Function(替换方法)
getter"getter"getter 函数Function(替换 getter)
setter"setter"setter 函数Function(替换 setter)
属性"field"undefined初始化函数 (initVal) => finalVal
accessor"accessor"{ get, set }{ get?, set?, init? }

注意:context.kind 的值是 "field",不是 "property"。它对应的是 class field 声明。

与 legacy 的本质差异

把两套装饰器放在一起看:

  1. 签名统一化:标准装饰器统一为 (value, context),不再因装饰目标不同而变来变去。
  2. 没有 descriptor:标准装饰器不需要操作属性描述对象,直接返回值替换即可。
  3. 没有参数装饰器:这是有意的设计决策——TC39 认为参数装饰器破坏了语言的”局部推理”能力。
  4. 属性装饰器可用:标准装饰器通过返回初始化函数解决了 legacy 属性装饰器”啥都干不了”的问题。
  5. accessor 关键字:标准装饰器的独家特性,配合 accessor 装饰器提供了最完整的拦截能力。

下一章我们聊聊实际项目里怎么选、怎么迁。