TypeScript 装饰器
1. 基本概念
装饰器模式
装饰器模式是一种结构型设计模式,它允许我们在运行时动态地向一个对象添加新的行为和职责,同时又不改变其结构。
这种模式的核心优势在于它遵循了“开闭原则”:对扩展开放,对修改关闭。我们可以在不修改现有代码的情况下,为其增添新功能。
JavaScript/TypeScript 中的装饰器
在 TypeScript 和 JavaScript (ES2022+) 中,装饰器是这一设计模式的语法糖。它是一种特殊的函数,以 @expression 的形式附加到类、方法、属性、访问器或参数上。expression 必须是一个求值后为函数的表达式。
@sealed
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}
// `sealed` 就是一个装饰器函数
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
2. 装饰器的两种标准
TypeScript 的装饰器经历了两个主要版本:
- 传统/实验性装饰器: 这是 TypeScript 早期的实现,在许多现有项目(如早期版本的 Angular, NestJS)中仍广泛使用。它功能强大但与最终的 JavaScript 标准存在差异。
- ES2022 标准装饰器: 这是被 TC39 委员会采纳并成为 JavaScript 官方标准的新版本。
要在 tsconfig.json 中使用传统装饰器,必须启用 experimentalDecorators 选项。现代装饰器在 target 为 ES2022 或更高版本时默认可用。
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true // 启用传统装饰器
}
}
3. 方法装饰器
基本概念
方法装饰器是最常用的一种,通常用于实现面向切面编程 (AOP)。
AOP (Aspect-Oriented Programming) 允许我们将那些横跨多个模块的“横切关注点”——如日志记录、性能监控、权限校验——分离出来,形成独立的“切面”,然后通过装饰器等方式嵌入到业务逻辑中,从而保持业务代码的纯净。
现代装饰器
现代方法装饰器接收两个参数:originalMethod (被装饰的原始方法) 和 context (一个包含方法元信息的上下文对象)。它必须返回一个新的函数来替换原始方法。
例如,如果我们想对一个方法添加鉴权逻辑:只有在 admin 时才执行该函数:
// AdminOnly.ts
export function AdminOnly<T extends (...args: any[]) => any>(
originalMethod: T,
context: {
kind: "method" | string;
name: string | symbol;
static?: boolean;
private?: boolean;
addInitializer?: (initializer: () => void) => void;
},
) {
// 仅在 method 上使用
if (context.kind !== "method") {
throw new Error("AdminOnly decorator can only be applied to methods");
}
// 返回新的方法实现以替换原始方法
return function (
this: any,
...args: Parameters<T>
): ReturnType<T> | undefined {
const currentUser = { role: "admin" }; // 模拟当前用户
console.log("正在进行权限检查...");
if (currentUser.role === "admin") {
console.log("权限检查通过!");
// 用原来的上下文和参数调用原方法
return originalMethod.apply(this, args);
} else {
console.error("Access Denied: Admins only.");
return undefined;
}
} as T;
}
传统装饰器 (Experimental)
传统方法装饰器接收三个参数:target (对于实例方法是类的原型,对于静态方法是类的构造函数)、propertyKey (方法名) 和 descriptor (属性描述符)。通过直接修改 descriptor.value 来包装原始方法。
function AdminOnly<T extends (...args: any[]) => any>(
target: any,
propertyKey: string,
descriptor: TypedPropertyDescriptor<T>,
) {
const originalMethod = descriptor.value!;
const wrapped: T = function (
this: any,
...args: Parameters<T>
): ReturnType<T> | undefined {
const currentUser = { role: "admin" };
if (currentUser.role === "admin") {
return originalMethod.apply(this, args) as ReturnType<T>;
} else {
console.error("Access Denied: Admins only.");
return undefined;
}
} as T;
descriptor.value = wrapped;
return descriptor;
}
4. 装饰器工厂
为了让装饰器更灵活,我们可以使用装饰器工厂。它就是一个返回装饰器函数的函数,允许我们向装饰器传递参数。
// 这是一个装饰器工厂
function Role(role: string) {
// 它返回一个传统的方法装饰器
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor,
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
// 模拟检查用户角色
const currentUser = { roles: ["user", "editor"] };
if (currentUser.roles.includes(role)) {
return originalMethod.apply(this, args);
} else {
console.error(`Access Denied: Requires role '${role}'.`);
}
};
};
}
class ArticleEditor {
@Role("editor")
editArticle(content: string) {
console.log("Article edited.");
}
@Role("admin")
publishArticle() {
console.log("Article published.");
}
}
const editor = new ArticleEditor();
editor.editArticle("new content"); // "Article edited."
editor.publishArticle(); // "Access Denied: Requires role 'admin'."
5. 类装饰器
类装饰器应用于类的构造函数,可用于观察、修改甚至替换整个类定义。
现代装饰器 (TS 5.0+)
接收 originalClass 和 context。可以返回一个新的类来继承并扩展原始类。
function AddVersion(originalClass: any, context: ClassDecoratorContext) {
// 返回一个继承自原始类的新类
return class extends originalClass {
version = "1.0.0"; // 添加新属性
constructor(...args: any[]) {
super(...args); // 调用父类构造函数
console.log(`${String(context.name)} class instantiated.`);
}
};
}
@AddVersion
class MyComponent {
name = "Component";
}
const comp = new MyComponent() as any;
// 输出: MyComponent class instantiated.
console.log(comp.version); // "1.0.0"
传统装饰器
只接收一个参数:类的构造函数。通常用于修改类的原型或添加静态属性。
function Frozen(constructor: Function) {
Object.freeze(constructor);
Object.freeze(constructor.prototype);
console.log(`${constructor.name} is now frozen.`);
}
@Frozen
class Config {
static setting = "A";
}
// Config.static.setting = "B"; // 运行时会报错,因为类已被冻结
6. 装饰器与元数据
元数据是“关于数据的数据”。装饰器最强大的应用之一就是读写元数据,这为依赖注入、ORM 等高级框架提供了基础。
ES 提案中包含了一个标准的元数据 API,通过 context.metadata 对象和 Symbol.metadata 访问。
function meta(key: string, value: any) {
return function (target: any, context: { metadata: any }) {
context.metadata[key] = value;
};
}
@meta("apiVersion", "2.0")
class ApiClient {
@meta("endpoint", "/users")
getUsers() {}
}
// 访问类和方法上的元数据
const metadata = (ApiClient as any)[Symbol.metadata];
console.log(metadata.apiVersion); // '2.0'
console.log(metadata.endpoint); // '/users'
在传统装饰器中,没有内置的元数据 API,因此社区广泛使用 reflect-metadata 这个库。我们需要先安装它并在项目入口处导入一次。它提供 Reflect.defineMetadata 和 Reflect.getMetadata 等方法来附加和读取元数据:
import "reflect-metadata";
// 定义元数据的 key
const requiredMetadataKey = Symbol("required");
function Required(target: any, propertyKey: string) {
// 为类的属性定义元数据
Reflect.defineMetadata(requiredMetadataKey, true, target, propertyKey);
}
class User {
@Required
name: string;
age?: number;
}
function validate(obj: any) {
const user = new obj();
for (let key in user) {
const isRequired = Reflect.getMetadata(requiredMetadataKey, user, key);
if (isRequired) {
console.log(`Property '${key}' is required.`);
}
}
}
validate(User); // "Property 'name' is required."