跳到主要内容

TypeScript 装饰器

装饰器是一种特殊的声明,可以附加到类声明、方法、访问器、属性或参数上,用于修改或添加功能。装饰器提供了一种优雅的方式来实现元编程。

两种装饰器标准

TypeScript 支持两种装饰器:

  1. 实验性装饰器(Legacy Decorators):TypeScript 早期实现的装饰器,需要启用 experimentalDecorators 编译选项
  2. 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))

  1. 从上到下评估每个装饰器表达式(工厂函数执行)
  2. 从下到上调用装饰器函数
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(): 被调用

类中装饰器的执行顺序

  1. 参数装饰器,然后是方法/访问器/属性装饰器,应用于每个实例成员
  2. 参数装饰器,然后是方法/访问器/属性装饰器,应用于每个静态成员
  3. 参数装饰器应用于构造函数
  4. 类装饰器应用于类

类装饰器

类装饰器在类声明之前声明,应用于类的构造函数,可以观察、修改或替换类定义。

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]

方法装饰器

方法装饰器在方法声明之前声明,应用于方法的属性描述符。

参数:

  1. 对于静态成员是类的构造函数,对于实例成员是类的原型
  2. 成员名称
  3. 成员的属性描述符
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 不允许同时装饰 getset,所有装饰器必须应用于文档顺序中的第一个访问器。

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;
}
}

属性装饰器

属性装饰器在属性声明之前声明。

参数:

  1. 对于静态成员是类的构造函数,对于实例成员是类的原型
  2. 成员名称

注意:属性描述符不作为参数提供,因为 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 是必需的

参数装饰器

参数装饰器在参数声明之前声明。

参数:

  1. 对于静态成员是类的构造函数,对于实例成员是类的原型
  2. 成员名称
  3. 参数在函数参数列表中的索引

参数装饰器只能用于观察参数是否被声明,返回值被忽略。

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 标准装饰器,原因如下:

  1. 官方标准:遵循 ECMAScript 规范,未来浏览器和 JavaScript 引擎将原生支持
  2. 更好的类型安全:TypeScript 5.0+ 对标准装饰器有完整的类型支持
  3. 更清晰的语义:上下文对象提供了丰富的元信息,代码更易理解
  4. 长期维护:实验性装饰器是过渡方案,未来可能逐渐被淘汰

两种装饰器对比总结

特性ECMAScript 标准装饰器实验性装饰器
TypeScript 版本5.0+早期版本支持
配置要求无需配置需要 experimentalDecorators
参数签名(target, context)多种签名,取决于装饰目标
类型推断完整类型支持需要 any 或复杂泛型
参数装饰器不支持支持
元数据发射不支持 emitDecoratorMetadata支持
自动访问器支持 accessor 关键字不支持
放置位置export 前后均可只能在 export
运行时依赖reflect-metadata(可选)

何时使用实验性装饰器

以下情况可能需要继续使用实验性装饰器:

  1. 现有代码库:大型项目迁移成本较高
  2. 依赖参数装饰器:标准装饰器目前不支持参数装饰
  3. 需要元数据:使用 emitDecoratorMetadata 进行依赖注入等场景
  4. 框架兼容性:某些框架(如 NestJS、TypeORM)目前仍使用实验性装饰器

迁移注意事项

如果决定从实验性装饰器迁移到标准装饰器,需要注意:

  1. 参数签名不同:需要重写所有装饰器函数
  2. 参数装饰器:标准装饰器不支持,需要寻找替代方案
  3. 元数据系统:如果依赖 reflect-metadata,需要考虑其他方案
  4. 渐进式迁移:两种装饰器不能混用在同一个类上

最佳实践

// 新项目推荐:使用标准装饰器
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 标准装饰器(推荐)

  1. 上下文对象提供的丰富元信息
  2. 类型安全的装饰器定义
  3. 自动访问器(accessor)
  4. 装饰器工厂和组合使用

实验性装饰器

  1. 类装饰器:修改或替换类定义
  2. 方法装饰器:修改方法行为
  3. 访问器装饰器:修改 getter/setter
  4. 属性装饰器:添加属性元数据
  5. 参数装饰器:验证方法参数
  6. 元数据系统:使用 reflect-metadata

实际应用:依赖注入、路由、验证等常见场景

练习

  1. 使用标准装饰器创建一个 @log 方法装饰器,记录方法调用和参数
  2. 使用标准装饰器创建一个 @bound 装饰器,自动绑定方法到实例
  3. 使用实验性装饰器创建一个数据验证装饰器(需要参数装饰器)
  4. 比较两种装饰器在实现相同功能时的代码差异