Skip to content

Legacy(experimentalDecorators)装饰器类型详解

概述

Legacy 装饰器是 TypeScript 通过 experimentalDecorators 编译选项提供的旧版装饰器实现, 基于早期的 TC39 提案(通常被称为 "Stage 1" 装饰器)。 它与 TypeScript 5.0+ 支持的 Stage 3 装饰器是完全不同的两套机制

启用方式:

json
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true
  }
}

Legacy 装饰器支持五种类型:Class、Method、Accessor、Property、Parameter。 其核心设计围绕 PropertyDescriptor(属性描述符)展开,与 Stage 3 装饰器基于函数替换的思路有本质区别。

装饰器类型总览

装饰器类型应用于参数返回值
Class 装饰器class C {}(constructor)新的构造函数 | void
Method 装饰器m() {}(target, propertyKey, descriptor)PropertyDescriptor | void
Accessor 装饰器get x() / set x()(target, propertyKey, descriptor)PropertyDescriptor | void
Property 装饰器x = 1(target, propertyKey)void(返回值被忽略)
Parameter 装饰器method(@dec arg)(target, propertyKey, parameterIndex)void(返回值被忽略)

各类型详细说明

1. Class 装饰器

ts
type ClassDecorator = <TFunction extends Function>(
  constructor: TFunction
) => TFunction | void;

参数:

  • constructor:被装饰类的构造函数

返回值含义:

  • 返回一个新的构造函数:替换原始类。需要注意,运行时不会自动维护原始原型链,开发者必须自行处理
  • 返回 void:保持原始类不变

示例:

ts
function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class BugReport {
  type = "report";
  title: string;
  constructor(t: string) {
    this.title = t;
  }
}

返回新构造函数的示例:

ts
function reportable<T extends { new (...args: any[]): {} }>(constructor: T) {
  // 返回新类替换原始类
  return class extends constructor {
    reportingURL = "http://www...";
  };
}

@reportable
class BugReport {
  title: string;
  constructor(t: string) {
    this.title = t;
  }
}

2. Method 装饰器

ts
type MethodDecorator = <T>(
  target: Object,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;

参数:

  • target:对于实例成员,是类的原型(Class.prototype);对于静态成员,是类的构造函数
  • propertyKey:方法名称
  • descriptor:该方法的属性描述符(PropertyDescriptor),包含 valuewritableenumerableconfigurable 等属性

注意:当编译目标低于 ES5 时,descriptorundefined

返回值含义:

  • 返回 PropertyDescriptor:用作该方法新的属性描述符,替换原始描述符
  • 返回 void:保持原始描述符不变

注意:当编译目标低于 ES5 时,返回值会被忽略。

示例:

ts
function enumerable(value: boolean) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    // 直接修改描述符
    descriptor.enumerable = value;
  };
}

class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }

  @enumerable(false)
  greet() {
    return "Hello, " + this.greeting;
  }
}

替换方法实现的示例:

ts
function log(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;
  // 通过修改 descriptor.value 替换方法
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey} with`, args);
    const result = originalMethod.apply(this, args);
    console.log(`Result:`, result);
    return result;
  };
  // 也可以返回新的描述符
  return descriptor;
}

3. Accessor 装饰器

ts
type AccessorDecorator = <T>(
  target: Object,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;

参数: 与 Method 装饰器完全相同。

  • target:类的原型或构造函数
  • propertyKey:访问器名称
  • descriptor:属性描述符,此时包含 get 和/或 set 函数

返回值含义: 与 Method 装饰器相同。

  • 返回 PropertyDescriptor:替换原始描述符
  • 返回 void:保持不变

重要区别: Legacy 装饰器中,getter 和 setter 共享同一个 PropertyDescriptor, 因此 TypeScript 禁止同时装饰同一个属性的 get 和 set。 装饰器必须应用在文档顺序中第一个出现的访问器上。 这与 Stage 3 装饰器中 getter/setter 分别独立装饰的设计完全不同。

示例:

ts
function configurable(value: boolean) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.configurable = value;
  };
}

class Point {
  private _x: number;
  private _y: number;
  constructor(x: number, y: number) {
    this._x = x;
    this._y = y;
  }

  // 装饰器只能放在第一个出现的访问器上
  @configurable(false)
  get x() {
    return this._x;
  }

  // 不能在这里再加 @configurable
  set x(value: number) {
    this._x = value;
  }
}

4. Property 装饰器

ts
type PropertyDecorator = (
  target: Object,
  propertyKey: string | symbol
) => void;

参数:

  • target:类的原型或构造函数
  • propertyKey:属性名称

注意:没有 PropertyDescriptor 参数。这是因为在 TypeScript 中, 实例属性在定义原型成员时还不存在,无法提供描述符,也无法观察或修改属性的初始化器。

返回值含义:

  • 返回值被忽略。Property 装饰器只能用于观察某个属性已被声明,不能修改属性行为
  • 通常配合 reflect-metadata 库记录元数据

示例:

ts
import "reflect-metadata";

const formatMetadataKey = Symbol("format");

function format(formatString: string) {
  // 利用 Reflect.metadata 记录元数据
  return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
  return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

class Greeter {
  @format("Hello, %s")
  greeting: string;

  constructor(message: string) {
    this.greeting = message;
  }

  greet() {
    let formatString = getFormat(this, "greeting");
    return formatString.replace("%s", this.greeting);
  }
}

5. Parameter 装饰器

ts
type ParameterDecorator = (
  target: Object,
  propertyKey: string | symbol | undefined,
  parameterIndex: number
) => void;

参数:

  • target:类的原型或构造函数
  • propertyKey:方法名称(如果装饰的是构造函数参数,则为 undefined
  • parameterIndex:参数在函数参数列表中的序号索引(从 0 开始)

返回值含义:

  • 返回值被忽略。Parameter 装饰器只能用于观察某个参数已被声明
  • 通常配合 reflect-metadata 和 Method 装饰器联合使用,实现参数验证等功能

示例:

ts
import "reflect-metadata";

const requiredMetadataKey = Symbol("required");

function required(
  target: Object,
  propertyKey: string | symbol,
  parameterIndex: number
) {
  // 记录哪些参数是必需的
  let existingRequiredParameters: number[] =
    Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
  existingRequiredParameters.push(parameterIndex);
  Reflect.defineMetadata(
    requiredMetadataKey,
    existingRequiredParameters,
    target,
    propertyKey
  );
}

function validate(
  target: any,
  propertyName: string,
  descriptor: TypedPropertyDescriptor<Function>
) {
  let method = descriptor.value!;
  descriptor.value = function () {
    let requiredParameters: number[] = Reflect.getOwnMetadata(
      requiredMetadataKey,
      target,
      propertyName
    );
    if (requiredParameters) {
      for (let parameterIndex of requiredParameters) {
        if (
          parameterIndex >= arguments.length ||
          arguments[parameterIndex] === undefined
        ) {
          throw new Error("Missing required argument.");
        }
      }
    }
    return method.apply(this, arguments);
  };
}

class BugReport {
  title: string;
  constructor(t: string) {
    this.title = t;
  }

  @validate
  print(@required verbose: boolean) {
    if (verbose) {
      return `title: ${this.title}`;
    } else {
      return this.title;
    }
  }
}

返回值规则总结

返回值类型适用的装饰器含义
新的构造函数Class替换原始类(需自行维护原型链)
PropertyDescriptorMethod、Accessor替换原始属性描述符
voidClass、Method、Accessor保持原始值不变
任意值(被忽略)Property、Parameter无法通过返回值产生任何效果

装饰器执行顺序

Legacy 装饰器有明确的执行顺序:

  1. 对每个实例成员:先执行 Parameter 装饰器,再执行 Method / Accessor / Property 装饰器
  2. 对每个静态成员:先执行 Parameter 装饰器,再执行 Method / Accessor / Property 装饰器
  3. 构造函数的 Parameter 装饰器
  4. Class 装饰器

多个装饰器应用于同一声明时:

  • 装饰器表达式从上到下求值
  • 装饰器函数从下到上调用(类似函数组合 f(g(x))

Legacy 装饰器 vs Stage 3 装饰器 关键差异

特性Legacy(experimentalDecorators)Stage 3(TC39 提案)
启用方式experimentalDecorators: trueTypeScript 5.0+ 默认支持
核心机制基于 PropertyDescriptor基于函数替换 + context 对象
Getter/Setter共享描述符,不能分别装饰独立装饰,互不影响
Property 装饰器无描述符,返回值被忽略可返回初始化器函数
Parameter 装饰器✅ 支持❌ 不支持(可能未来扩展)
accessor 关键字❌ 不支持✅ 支持 Auto-Accessor
addInitializer❌ 不支持✅ 所有类型都支持
context 对象❌ 无✅ 提供 kind、name、access 等
reflect-metadata常配合使用提案内置 metadata 支持
规范状态非标准,TypeScript 专有实现ECMAScript 标准提案

参考资料