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),包含value、writable、enumerable、configurable等属性
注意:当编译目标低于 ES5 时,
descriptor为undefined。
返回值含义:
- 返回
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 | 替换原始类(需自行维护原型链) |
PropertyDescriptor | Method、Accessor | 替换原始属性描述符 |
void | Class、Method、Accessor | 保持原始值不变 |
| 任意值(被忽略) | Property、Parameter | 无法通过返回值产生任何效果 |
装饰器执行顺序
Legacy 装饰器有明确的执行顺序:
- 对每个实例成员:先执行 Parameter 装饰器,再执行 Method / Accessor / Property 装饰器
- 对每个静态成员:先执行 Parameter 装饰器,再执行 Method / Accessor / Property 装饰器
- 构造函数的 Parameter 装饰器
- Class 装饰器
多个装饰器应用于同一声明时:
- 装饰器表达式从上到下求值
- 装饰器函数从下到上调用(类似函数组合
f(g(x)))
Legacy 装饰器 vs Stage 3 装饰器 关键差异
| 特性 | Legacy(experimentalDecorators) | Stage 3(TC39 提案) |
|---|---|---|
| 启用方式 | experimentalDecorators: true | TypeScript 5.0+ 默认支持 |
| 核心机制 | 基于 PropertyDescriptor | 基于函数替换 + context 对象 |
| Getter/Setter | 共享描述符,不能分别装饰 | 独立装饰,互不影响 |
| Property 装饰器 | 无描述符,返回值被忽略 | 可返回初始化器函数 |
| Parameter 装饰器 | ✅ 支持 | ❌ 不支持(可能未来扩展) |
accessor 关键字 | ❌ 不支持 | ✅ 支持 Auto-Accessor |
addInitializer | ❌ 不支持 | ✅ 所有类型都支持 |
context 对象 | ❌ 无 | ✅ 提供 kind、name、access 等 |
reflect-metadata | 常配合使用 | 提案内置 metadata 支持 |
| 规范状态 | 非标准,TypeScript 专有实现 | ECMAScript 标准提案 |