TypeScript 开发指南 / 13 - 装饰器
装饰器
装饰器(Decorators)是一种特殊的声明,可以附加到类声明、方法、属性或参数上,用于修改或扩展它们的行为。
注意:TypeScript 5.0+ 支持 ECMAScript 标准装饰器。本文档介绍的是标准装饰器语法。旧版实验性装饰器(
experimentalDecorators)在 NestJS 等框架中仍广泛使用。
启用装饰器
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true // 旧版实验性装饰器
}
}
TypeScript 5.0+ 的标准装饰器不需要特殊配置,只需 target: "ES2022" 或更高。
类装饰器(Class Decorators)
// 类装饰器接收类构造函数作为参数
function Sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@Sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return `Hello, ${this.greeting}`;
}
}
// 装饰器工厂(返回装饰器函数的函数)
function Logger(prefix: string) {
return function (constructor: Function) {
console.log(`[${prefix}] Creating instance of ${constructor.name}`);
};
}
@Logger("APP")
class User {
constructor(public name: string) {}
}
// 输出: [APP] Creating instance of User
类装饰器的返回值
// 返回新类来替换原始类
function Mixin(...mixins: any[]) {
return function <T extends new (...args: any[]) => any>(constructor: T) {
return class extends constructor {
constructor(...args: any[]) {
super(...args);
mixins.forEach(mixin => {
Object.assign(this, new mixin());
});
}
};
};
}
class Serializable {
serialize() {
return JSON.stringify(this);
}
}
@Mixin(Serializable)
class User {
constructor(public name: string) {}
}
const user = new User("Alice");
console.log(user.serialize()); // '{"name":"Alice"}'
方法装饰器(Method Decorators)
// 方法装饰器参数:
// target: 对于静态成员是类构造函数,对于实例成员是类原型
// propertyKey: 方法名
// descriptor: 属性描述符
function Log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with args:`, args);
const result = originalMethod.apply(this, args);
console.log(`${propertyKey} returned:`, result);
return result;
};
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
@Log
multiply(a: number, b: number): number {
return a * b;
}
}
const calc = new Calculator();
calc.add(1, 2);
// Calling add with args: [1, 2]
// add returned: 3
方法装饰器的返回值
// 返回新的 PropertyDescriptor
function Throttle(delay: number) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
let lastCall = 0;
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
return originalMethod.apply(this, args);
}
};
return descriptor;
};
}
class SearchService {
@Throttle(300)
search(query: string) {
console.log(`Searching: ${query}`);
}
}
属性装饰器(Property Decorators)
// 属性装饰器参数:
// target: 对于静态成员是类构造函数,对于实例成员是类原型
// propertyKey: 属性名
function Required(target: any, propertyKey: string) {
let value: any;
const getter = () => value;
const setter = (newVal: any) => {
if (newVal === undefined || newVal === null) {
throw new Error(`${propertyKey} is required`);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class User {
@Required
name!: string;
@Required
email!: string;
age?: number;
}
const user = new User();
user.name = "Alice"; // ✅
// user.name = null!; // ❌ Error: name is required
参数装饰器(Parameter Decorators)
// 参数装饰器参数:
// target: 类原型
// propertyKey: 方法名
// parameterIndex: 参数索引
function Validate(
target: any,
propertyKey: string,
parameterIndex: number
) {
const existingValidators: number[] =
Reflect.getOwnMetadata("validators", target, propertyKey) || [];
existingValidators.push(parameterIndex);
Reflect.defineMetadata("validators", existingValidators, target, propertyKey);
}
class UserService {
createUser(@Validate name: string, @Validate email: string, age?: number) {
// ...
}
}
装饰器组合与执行顺序
function First() {
console.log("First(): factory evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("First(): called");
};
}
function Second() {
console.log("Second(): factory evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("Second(): called");
};
}
class Example {
@First()
@Second()
method() {}
}
// 执行顺序:
// First(): factory evaluated
// Second(): factory evaluated
// Second(): called
// First(): called
// 从下往上执行
实用装饰器示例
Memoize 装饰器
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)) {
return cache.get(key);
}
const result = originalMethod.apply(this, args);
cache.set(key, result);
return result;
};
return descriptor;
}
class MathService {
@Memoize
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
Retry 装饰器
function Retry(maxAttempts: number = 3, delay: number = 1000) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
lastError = error as Error;
if (attempt < maxAttempts) {
await new Promise(r => setTimeout(r, delay * attempt));
}
}
}
throw lastError!;
};
return descriptor;
};
}
class ApiService {
@Retry(3, 1000)
async fetchData(url: string): Promise<any> {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
}
Deprecated 装饰器
function Deprecated(message?: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.warn(
`⚠️ ${propertyKey} is deprecated. ${message || "Use alternative method."}`
);
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class LegacyService {
@Deprecated("Use newMethod() instead")
oldMethod() {
// ...
}
}
装饰器实现依赖注入
// 简单的依赖注入容器
const container = new Map<string, any>();
function Injectable(key: string) {
return function (constructor: any) {
container.set(key, new constructor());
};
}
function Inject(key: string) {
return function (target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
get: () => container.get(key),
enumerable: true,
configurable: true
});
};
}
@Injectable("Logger")
class Logger {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
@Injectable("UserService")
class UserService {
@Inject("Logger")
private logger!: Logger;
getUser(id: number) {
this.logger.log(`Getting user ${id}`);
// ...
}
}
与框架配合使用
NestJS 风格的装饰器
// 路由装饰器
function Controller(prefix: string) {
return function (constructor: Function) {
Reflect.defineMetadata("prefix", prefix, constructor);
};
}
function Get(path: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
Reflect.defineMetadata("method", "GET", target, propertyKey);
Reflect.defineMetadata("path", path, target, propertyKey);
};
}
function Post(path: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
Reflect.defineMetadata("method", "POST", target, propertyKey);
Reflect.defineMetadata("path", path, target, propertyKey);
};
}
@Controller("/api/users")
class UserController {
@Get("/")
getAll() {
return [];
}
@Post("/")
create() {
return {};
}
}
业务场景:表单验证
// 验证装饰器
function MinLength(min: number) {
return function (target: any, propertyKey: string) {
let value: string;
Object.defineProperty(target, propertyKey, {
get: () => value,
set: (newVal: string) => {
if (newVal.length < min) {
throw new Error(`${propertyKey} must be at least ${min} characters`);
}
value = newVal;
},
enumerable: true,
configurable: true
});
};
}
function Email(target: any, propertyKey: string) {
let value: string;
Object.defineProperty(target, propertyKey, {
get: () => value,
set: (newVal: string) => {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newVal)) {
throw new Error(`${propertyKey} must be a valid email`);
}
value = newVal;
},
enumerable: true,
configurable: true
});
}
class RegistrationForm {
@MinLength(3)
username!: string;
@Email
email!: string;
@MinLength(8)
password!: string;
}
const form = new RegistrationForm();
form.username = "alice"; // ✅
form.email = "[email protected]"; // ✅
// form.username = "ab"; // ❌ Error: username must be at least 3 characters
注意事项
- 装饰器的执行顺序:工厂函数从上到下,装饰器本身从下到上
- 属性描述符:方法装饰器可以通过返回新的
PropertyDescriptor来替换方法 - 元数据反射:使用
Reflect.metadata需要引入reflect-metadata库 - 标准 vs 实验性:TypeScript 5.0+ 的标准装饰器语法与旧版实验性装饰器不同
- 框架选择:NestJS、Angular 等框架大量使用实验性装饰器