TypeScript 装饰器
装饰器是一种特殊的声明,可以附加到类声明、方法、访问器、属性或参数上,用于修改或添加功能。装饰器提供了一种优雅的方式来实现元编程。
两种装饰器标准
TypeScript 支持两种装饰器:
- 实验性装饰器(Legacy Decorators):TypeScript 早期实现的装饰器,需要启用
experimentalDecorators编译选项 - ECMAScript 标准装饰器:TypeScript 5.0 引入的新标准,遵循 ECMAScript Stage 3 提案
本章将分别介绍这两种装饰器。如果你是新项目,建议使用 ECMAScript 标准装饰器。
ECMAScript 标准装饰器(推荐)
TypeScript 5.0 开始原生支持 ECMAScript 标准装饰器,这是未来的主流方向。与实验性装饰器相比,标准装饰器有更好的类型支持和更清晰的语义。
基本语法
标准装饰器是一个函数,接收两个参数:被装饰的目标和上下文对象。
// 方法定义装饰器
function loggedMethod(
originalMethod: any,
context: ClassMethodDecoratorContext
) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`调用方法 ${methodName}`);
const result = originalMethod.call(this, ...args);
console.log(`方法 ${methodName} 返回 ${result}`);
return result;
}
return replacementMethod;
}
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@loggedMethod
greet() {
return `你好,我是 ${this.name}`;
}
}
const person = new Person("张三");
person.greet();
// 输出:
// 调用方法 greet
// 方法 greet 返回 你好,我是 张三
解释:标准装饰器接收两个参数:
originalMethod:被装饰的方法context:上下文对象,包含装饰目标的元信息
上下文对象(Context)
上下文对象提供了丰富的元信息:
interface ClassMethodDecoratorContext {
kind: "method"; // 装饰目标类型
name: string | symbol; // 方法名称
static: boolean; // 是否为静态方法
private: boolean; // 是否为私有方法
access: { // 访问器对象
has(object: any): boolean;
get(object: any): any;
};
addInitializer(initializer: () => void): void; // 添加初始化器
}
装饰器工厂
可以创建返回装饰器的工厂函数来配置装饰器行为:
function log(prefix: string) {
return function loggedMethod(
originalMethod: any,
context: ClassMethodDecoratorContext
) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`${prefix}: 调用 ${methodName}`);
return originalMethod.call(this, ...args);
}
return replacementMethod;
};
}
class Calculator {
@log("DEBUG")
add(a: number, b: number) {
return a + b;
}
@log("INFO")
multiply(a: number, b: number) {
return a * b;
}
}
装饰器组合
多个装饰器可以组合使用,执行顺序是从下到上、从内到外:
function first(
value: any,
context: ClassMethodDecoratorContext
) {
console.log("first 执行");
return value;
}
function second(
value: any,
context: ClassMethodDecoratorContext
) {
console.log("second 执行");
return value;
}
class Example {
@first
@second
method() {}
}
// 输出:
// second 执行
// first 执行
绑定 this 的装饰器
使用 addInitializer 可以在构造函数执行时自动绑定 this:
function bound(
originalMethod: any,
context: ClassMethodDecoratorContext
) {
const methodName = String(context.name);
context.addInitializer(function (this: any) {
this[methodName] = this[methodName].bind(this);
});
}
class Logger {
prefix = "LOG";
@bound
log(message: string) {
console.log(`${this.prefix}: ${message}`);
}
}
const logger = new Logger();
const logFn = logger.log;
logFn("测试"); // 正常工作,this 已绑定
完整类型定义的装饰器
为了获得完整的类型安全,可以定义泛型装饰器:
function loggedMethod<This, Args extends any[], Return>(
originalMethod: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
const methodName = String(context.name);
function replacementMethod(this: This, ...args: Args): Return {
console.log(`调用 ${methodName},参数:${JSON.stringify(args)}`);
const result = originalMethod.call(this, ...args);
console.log(`${methodName} 返回:${JSON.stringify(result)}`);
return result;
}
return replacementMethod;
}
class MathOps {
@loggedMethod
add(a: number, b: number): number {
return a + b;
}
@loggedMethod
greet(name: string): string {
return `你好,${name}`;
}
}
类装饰器
标准装饰器可以装饰整个类:
function sealed<T extends { new(...args: any[]): any }>(
target: T,
context: ClassDecoratorContext
) {
console.log(`密封类 ${String(context.name)}`);
Object.seal(target);
Object.seal(target.prototype);
}
function reportable<T extends { new(...args: any[]): any }>(
target: T,
context: ClassDecoratorContext
) {
return class extends target {
reportingURL = "https://example.com/report";
};
}
@sealed
@reportable
class BugReport {
type = "report";
title: string;
constructor(title: string) {
this.title = title;
}
}
字段装饰器
装饰类字段:
function uppercase(
target: undefined,
context: ClassFieldDecoratorContext
) {
return function (initialValue: string) {
return initialValue.toUpperCase();
};
}
class Person {
@uppercase
name = "john"; // 实际值为 "JOHN"
}
Getter/Setter 装饰器
function validate(
target: any,
context: ClassGetterDecoratorContext
) {
return function (this: any) {
const value = target.get.call(this);
if (value === null || value === undefined) {
throw new Error(`${String(context.name)} 不能为空`);
}
return value;
};
}
class Config {
private _apiKey: string;
constructor(key: string) {
this._apiKey = key;
}
@validate
get apiKey() {
return this._apiKey;
}
}
自动访问器(Auto-Accessor)
TypeScript 5.0 支持自动访问器,使用 accessor 关键字声明:
class Person {
// 自动创建私有字段和 getter/setter
accessor name: string;
accessor age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
// 等价于:
class PersonEquivalent {
#__name: string;
#__age: number;
get name() { return this.#__name; }
set name(value) { this.#__name = value; }
get age() { return this.#__age; }
set age(value) { this.#__age = value; }
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
装饰自动访问器:
function logged(
target: any,
context: ClassAccessorDecoratorContext
) {
return {
get(this: any) {
console.log(`获取 ${String(context.name)}`);
return target.get.call(this);
},
set(this: any, value: any) {
console.log(`设置 ${String(context.name)} = ${value}`);
target.set.call(this, value);
}
};
}
class Counter {
@logged
accessor count = 0;
}
与实验性装饰器的区别
| 特性 | 标准装饰器 | 实验性装饰器 |
|---|---|---|
| 启用方式 | 默认启用 | 需要 experimentalDecorators |
| 参数签名 | (target, context) | 多种签名 |
| 元数据 | 不支持 emitDecoratorMetadata | 支持元数据发射 |
| 参数装饰器 | 不支持 | 支持 |
| 类型安全 | 更好的类型推断 | 类型相对较弱 |
| 放置位置 | export 前后均可 | 只能在 export 前 |
实验性装饰器(Legacy)
实验性装饰器是 TypeScript 早期实现的装饰器,需要手动启用。
启用实验性装饰器
在 tsconfig.json 中启用:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
装饰器基础
装饰器使用 @expression 形式,其中 expression 必须求值为一个函数,该函数在运行时被调用,并传入被装饰声明的信息。
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}
解释:@sealed 装饰器会密封类及其原型,防止运行时添加或删除属性。
装饰器工厂
装饰器工厂是一个返回装饰器函数的函数,用于自定义装饰器的行为:
function color(value: string) {
// 装饰器工厂
return function (target: any, propertyKey: string) {
// 装饰器函数
console.log(`${propertyKey} 的颜色设置为 ${value}`);
};
}
class Button {
@color("blue")
background: string;
}
// 输出:background 的颜色设置为 blue
解释:装饰器工厂允许传递参数,使装饰器更灵活可配置。
装饰器组合
多个装饰器可以同时应用到一个声明上:
// 单行写法
@f @g x
// 多行写法
@f
@g
x
执行顺序
装饰器的执行遵循数学函数组合的规则 (f ∘ g)(x) 等价于 f(g(x)):
- 从上到下评估每个装饰器表达式(工厂函数执行)
- 从下到上调用装饰器函数
function first() {
console.log("first(): 工厂评估");
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log("first(): 被调用");
};
}
function second() {
console.log("second(): 工厂评估");
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log("second(): 被调用");
};
}
class ExampleClass {
@first()
@second()
method() {}
}
// 输出顺序:
// first(): 工厂评估
// second(): 工厂评估
// second(): 被调用
// first(): 被调用
类中装饰器的执行顺序
- 参数装饰器,然后是方法/访问器/属性装饰器,应用于每个实例成员
- 参数装饰器,然后是方法/访问器/属性装饰器,应用于每个静态成员
- 参数装饰器应用于构造函数
- 类装饰器应用于类
类装饰器
类装饰器在类声明之前声明,应用于类的构造函数,可以观察、修改或替换类定义。
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}
替换类构造函数
类装饰器可以返回一个新的构造函数来替换原始类:
function reportableClassDecorator<T extends { new (...args: any[]): {} }>(
constructor: T
) {
return class extends constructor {
reportingURL = "http://www.example.com/report";
};
}
@reportableClassDecorator
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}
const bug = new BugReport("需要暗黑模式");
console.log(bug.title); // "需要暗黑模式"
console.log(bug.type); // "report"
// console.log(bug.reportingURL); // 存在,但 TypeScript 类型不知道
解释:装饰器返回的新类不会改变 TypeScript 的类型推断,所以需要额外声明类型。
实际应用示例
// 日志装饰器
function logged(constructor: any) {
console.log(`类 ${constructor.name} 已被创建`);
// 保存原始构造函数
const original = constructor;
// 创建新的构造函数
const newConstructor: any = function (...args: any[]) {
console.log(`创建 ${original.name} 实例,参数:`, args);
return new original(...args);
};
// 复制原型
newConstructor.prototype = original.prototype;
return newConstructor;
}
@logged
class Person {
constructor(public name: string, public age: number) {}
}
const person = new Person("张三", 25);
// 输出:
// 类 Person 已被创建
// 创建 Person 实例,参数: ["张三", 25]
方法装饰器
方法装饰器在方法声明之前声明,应用于方法的属性描述符。
参数:
- 对于静态成员是类的构造函数,对于实例成员是类的原型
- 成员名称
- 成员的属性描述符
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;
}
}
const greeter = new Greeter("world");
for (const key in greeter) {
console.log(key); // 只输出 "greeting",不输出 "greet"
}
方法执行时机装饰器
function measure(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`${propertyKey} 执行耗时: ${(end - start).toFixed(2)}ms`);
return result;
};
return descriptor;
}
class Calculator {
@measure
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
const calc = new Calculator();
calc.fibonacci(30); // 输出执行耗时
方法缓存装饰器
function memoize(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
const cache = new Map<string, any>();
descriptor.value = function (...args: any[]) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log(`从缓存返回 ${propertyKey}(${key})`);
return cache.get(key);
}
const result = originalMethod.apply(this, args);
cache.set(key, result);
return result;
};
return descriptor;
}
class MathOperations {
@memoize
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
const math = new MathOperations();
math.fibonacci(10); // 计算
math.fibonacci(10); // 从缓存返回
访问器装饰器
访问器装饰器应用于访问器的属性描述符。注意:TypeScript 不允许同时装饰 get 和 set,所有装饰器必须应用于文档顺序中的第一个访问器。
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(false)
get y() {
return this._y;
}
}
属性装饰器
属性装饰器在属性声明之前声明。
参数:
- 对于静态成员是类的构造函数,对于实例成员是类的原型
- 成员名称
注意:属性描述符不作为参数提供,因为 TypeScript 没有机制在定义原型成员时描述实例属性。返回值也会被忽略。
import "reflect-metadata";
const formatMetadataKey = Symbol("format");
function format(formatString: string) {
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() {
const formatString = getFormat(this, "greeting");
return formatString.replace("%s", this.greeting);
}
}
const greeter = new Greeter("世界");
console.log(greeter.greet()); // "Hello, 世界"
属性验证装饰器
function required(target: object, propertyKey: string | symbol) {
// 注册需要验证的属性
const requiredKeys = Reflect.getOwnMetadata("required", target) || [];
requiredKeys.push(propertyKey);
Reflect.defineMetadata("required", requiredKeys, target);
}
function validate(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const requiredKeys = Reflect.getOwnMetadata("required", this) || [];
for (const key of requiredKeys) {
if (this[key] === undefined || this[key] === null) {
throw new Error(`属性 ${String(key)} 是必需的`);
}
}
return originalMethod.apply(this, args);
};
return descriptor;
}
class User {
@required
name!: string;
@required
email!: string;
age?: number;
@validate
save() {
console.log(`保存用户: ${this.name}`);
}
}
const user = new User();
user.name = "张三";
// user.email = "[email protected]"; // 忘记设置
// user.save(); // 错误:属性 email 是必需的
参数装饰器
参数装饰器在参数声明之前声明。
参数:
- 对于静态成员是类的构造函数,对于实例成员是类的原型
- 成员名称
- 参数在函数参数列表中的索引
参数装饰器只能用于观察参数是否被声明,返回值被忽略。
import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
function required(
target: Object,
propertyKey: string | symbol,
parameterIndex: number
) {
const 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>
) {
const method = descriptor.value!;
descriptor.value = function (...args: any[]) {
const requiredParameters: number[] = Reflect.getOwnMetadata(
requiredMetadataKey,
target,
propertyName
);
if (requiredParameters) {
for (const parameterIndex of requiredParameters) {
if (
parameterIndex >= args.length ||
args[parameterIndex] === undefined
) {
throw new Error("缺少必需参数");
}
}
}
return method.apply(this, args);
};
}
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
@validate
print(@required verbose: boolean) {
if (verbose) {
return `type: ${this.type}\ntitle: ${this.title}`;
} else {
return this.title;
}
}
}
const report = new BugReport("测试报告");
// report.print(undefined as any); // 错误:缺少必需参数
report.print(true); // OK
元数据
reflect-metadata 库提供了元数据 API 的 polyfill:
npm install reflect-metadata
在入口文件导入:
import "reflect-metadata";
使用元数据
import "reflect-metadata";
const metadataKey = Symbol("design:type");
class Example {
// TypeScript 自动注入设计时类型信息
name: string = "hello";
method(value: number): string {
return value.toString();
}
}
const example = new Example();
// 获取属性类型
const type = Reflect.getMetadata("design:type", example, "name");
console.log(type); // [Function: String]
// 获取返回类型
const returnType = Reflect.getMetadata("design:returntype", example, "method");
console.log(returnType); // [Function: String]
// 获取参数类型
const paramTypes = Reflect.getMetadata("design:paramtypes", example, "method");
console.log(paramTypes); // [[Function: Number]]
启用元数据发射
在 tsconfig.json 中:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
实际应用场景
1. 依赖注入
import "reflect-metadata";
type Constructor<T = any> = new (...args: any[]) => T;
const INJECTABLE_TOKEN = Symbol("injectable");
function Injectable() {
return function (target: Constructor) {
Reflect.defineMetadata(INJECTABLE_TOKEN, true, target);
};
}
function Inject(token?: string) {
return function (
target: any,
propertyKey: string | symbol | undefined,
parameterIndex: number
) {
const existingTokens =
Reflect.getOwnMetadata("inject:tokens", target) || [];
existingTokens[parameterIndex] = token;
Reflect.defineMetadata("inject:tokens", existingTokens, target);
};
}
@Injectable()
class DatabaseService {
query(sql: string) {
console.log(`执行查询: ${sql}`);
return [{ id: 1, name: "张三" }];
}
}
@Injectable()
class UserService {
constructor(@Inject() private db: DatabaseService) {}
getUsers() {
return this.db.query("SELECT * FROM users");
}
}
// 简单的容器
class Container {
private instances = new Map();
resolve<T>(target: Constructor<T>): T {
if (this.instances.has(target)) {
return this.instances.get(target);
}
const paramTypes: Constructor[] =
Reflect.getMetadata("design:paramtypes", target) || [];
const injections = paramTypes.map((param) => this.resolve(param));
const instance = new target(...injections);
this.instances.set(target, instance);
return instance;
}
}
const container = new Container();
const userService = container.resolve(UserService);
userService.getUsers(); // 执行查询: SELECT * FROM users
2. API 路由装饰器
import "reflect-metadata";
const METHOD_METADATA = Symbol("method");
const PATH_METADATA = Symbol("path");
function Get(path: string): MethodDecorator {
return (
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) => {
Reflect.defineMetadata(METHOD_METADATA, "GET", descriptor.value);
Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
};
}
function Post(path: string): MethodDecorator {
return (
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) => {
Reflect.defineMetadata(METHOD_METADATA, "POST", descriptor.value);
Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
};
}
function Controller(path: string): ClassDecorator {
return (target: any) => {
Reflect.defineMetadata(PATH_METADATA, path, target);
};
}
@Controller("/users")
class UserController {
@Get("/")
getAll() {
return { users: [] };
}
@Get("/:id")
getById(id: string) {
return { id, name: "张三" };
}
@Post("/")
create(data: any) {
return { success: true, data };
}
}
// 收集路由
function registerRoutes(controller: any) {
const basePath = Reflect.getMetadata(PATH_METADATA, controller.constructor);
const methods = Object.getOwnPropertyNames(
Object.getPrototypeOf(controller)
).filter((m) => m !== "constructor");
const routes: any[] = [];
for (const methodName of methods) {
const method = (controller as any)[methodName];
const httpMethod = Reflect.getMetadata(METHOD_METADATA, method);
const path = Reflect.getMetadata(PATH_METADATA, method);
if (httpMethod && path) {
routes.push({
method: httpMethod,
path: basePath + path,
handler: method.bind(controller),
});
}
}
return routes;
}
const controller = new UserController();
const routes = registerRoutes(controller);
console.log(routes);
// [
// { method: 'GET', path: '/users/', handler: [Function: getAll] },
// { method: 'GET', path: '/users/:id', handler: [Function: getById] },
// { method: 'POST', path: '/users/', handler: [Function: create] }
// ]
3. 数据验证装饰器
import "reflect-metadata";
function validateType(expectedType: string) {
return function (target: any, propertyKey: string) {
const existingValidations =
Reflect.getMetadata("validations", target) || {};
existingValidations[propertyKey] = {
...existingValidations[propertyKey],
type: expectedType,
};
Reflect.defineMetadata("validations", existingValidations, target);
};
}
function minLength(length: number) {
return function (target: any, propertyKey: string) {
const existingValidations =
Reflect.getMetadata("validations", target) || {};
existingValidations[propertyKey] = {
...existingValidations[propertyKey],
minLength: length,
};
Reflect.defineMetadata("validations", existingValidations, target);
};
}
function maxLength(length: number) {
return function (target: any, propertyKey: string) {
const existingValidations =
Reflect.getMetadata("validations", target) || {};
existingValidations[propertyKey] = {
...existingValidations[propertyKey],
maxLength: length,
};
Reflect.defineMetadata("validations", existingValidations, target);
};
}
function range(min: number, max: number) {
return function (target: any, propertyKey: string) {
const existingValidations =
Reflect.getMetadata("validations", target) || {};
existingValidations[propertyKey] = {
...existingValidations[propertyKey],
range: { min, max },
};
Reflect.defineMetadata("validations", existingValidations, target);
};
}
function validate<T extends object>(obj: T): { valid: boolean; errors: string[] } {
const validations = Reflect.getMetadata("validations", obj) || {};
const errors: string[] = [];
for (const [property, rules] of Object.entries(validations) as [string, any][]) {
const value = (obj as any)[property];
if (rules.type && typeof value !== rules.type) {
errors.push(`${property} 必须是 ${rules.type} 类型`);
}
if (rules.minLength && value.length < rules.minLength) {
errors.push(`${property} 长度不能少于 ${rules.minLength}`);
}
if (rules.maxLength && value.length > rules.maxLength) {
errors.push(`${property} 长度不能超过 ${rules.maxLength}`);
}
if (rules.range && (value < rules.range.min || value > rules.range.max)) {
errors.push(
`${property} 必须在 ${rules.range.min} 到 ${rules.range.max} 之间`
);
}
}
return { valid: errors.length === 0, errors };
}
class User {
@validateType("string")
@minLength(2)
@maxLength(20)
name!: string;
@validateType("number")
@range(0, 150)
age!: number;
@validateType("string")
@minLength(5)
@maxLength(100)
email!: string;
}
const user = new User();
user.name = "张";
user.age = 200;
user.email = "a@b";
const result = validate(user);
console.log(result);
// {
// valid: false,
// errors: [
// 'name 长度不能少于 2',
// 'age 必须在 0 到 150 之间',
// 'email 长度不能少于 5'
// ]
// }
选择建议与迁移指南
新项目应该选择哪种装饰器?
推荐使用 ECMAScript 标准装饰器,原因如下:
- 官方标准:遵循 ECMAScript 规范,未来浏览器和 JavaScript 引擎将原生支持
- 更好的类型安全:TypeScript 5.0+ 对标准装饰器有完整的类型支持
- 更清晰的语义:上下文对象提供了丰富的元信息,代码更易理解
- 长期维护:实验性装饰器是过渡方案,未来可能逐渐被淘汰
两种装饰器对比总结
| 特性 | ECMAScript 标准装饰器 | 实验性装饰器 |
|---|---|---|
| TypeScript 版本 | 5.0+ | 早期版本支持 |
| 配置要求 | 无需配置 | 需要 experimentalDecorators |
| 参数签名 | (target, context) | 多种签名,取决于装饰目标 |
| 类型推断 | 完整类型支持 | 需要 any 或复杂泛型 |
| 参数装饰器 | 不支持 | 支持 |
| 元数据发射 | 不支持 emitDecoratorMetadata | 支持 |
| 自动访问器 | 支持 accessor 关键字 | 不支持 |
| 放置位置 | export 前后均可 | 只能在 export 前 |
| 运行时依赖 | 无 | reflect-metadata(可选) |
何时使用实验性装饰器
以下情况可能需要继续使用实验性装饰器:
- 现有代码库:大型项目迁移成本较高
- 依赖参数装饰器:标准装饰器目前不支持参数装饰
- 需要元数据:使用
emitDecoratorMetadata进行依赖注入等场景 - 框架兼容性:某些框架(如 NestJS、TypeORM)目前仍使用实验性装饰器
迁移注意事项
如果决定从实验性装饰器迁移到标准装饰器,需要注意:
- 参数签名不同:需要重写所有装饰器函数
- 参数装饰器:标准装饰器不支持,需要寻找替代方案
- 元数据系统:如果依赖
reflect-metadata,需要考虑其他方案 - 渐进式迁移:两种装饰器不能混用在同一个类上
最佳实践
// 新项目推荐:使用标准装饰器
function logged(
target: any,
context: ClassMethodDecoratorContext
) {
// 标准装饰器实现
}
class NewProject {
@logged
method() {}
}
// 如果框架要求:使用实验性装饰器
// tsconfig.json: { "experimentalDecorators": true }
function loggedLegacy(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
// 实验性装饰器实现
}
class LegacyProject {
@loggedLegacy
method() {}
}
小结
本章我们学习了 TypeScript 装饰器:
ECMAScript 标准装饰器(推荐):
- 上下文对象提供的丰富元信息
- 类型安全的装饰器定义
- 自动访问器(accessor)
- 装饰器工厂和组合使用
实验性装饰器:
- 类装饰器:修改或替换类定义
- 方法装饰器:修改方法行为
- 访问器装饰器:修改 getter/setter
- 属性装饰器:添加属性元数据
- 参数装饰器:验证方法参数
- 元数据系统:使用
reflect-metadata
实际应用:依赖注入、路由、验证等常见场景
练习
- 使用标准装饰器创建一个
@log方法装饰器,记录方法调用和参数 - 使用标准装饰器创建一个
@bound装饰器,自动绑定方法到实例 - 使用实验性装饰器创建一个数据验证装饰器(需要参数装饰器)
- 比较两种装饰器在实现相同功能时的代码差异