TypeScript 装饰器完全指南
深入理解 TypeScript 装饰器的原理与应用,掌握这一强大的元编程工具。
什么是装饰器
装饰器是一种特殊的声明,可以附加到类、方法、属性或参数上,用于修改其行为。它是 TypeScript 中的元编程工具,让我们能够在编译时动态修改代码结构。
核心价值: 将横切关注点(如日志、验证、缓存)从业务逻辑中分离出来,实现代码的声明式复用。
typescript
// ✅ 装饰器方式:声明式、可复用
function Log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[LOG] ${key} 被调用`);
return original.apply(this, args);
};
}
class UserService {
@Log
createUser() {
// 业务逻辑...
}
}装饰器基础
启用装饰器
json
// tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}装饰器类型
| 装饰器类型 | 应用目标 | 常见用途 |
|---|---|---|
| 类装饰器 | class | 单例模式、日志、Mixin |
| 方法装饰器 | 方法 | 日志、性能监控、防抖 |
| 属性装饰器 | 属性 | 验证、依赖注入 |
| 参数装饰器 | 参数 | 验证、参数映射 |
| 访问器装饰器 | getter/setter | 缓存、验证 |
执行顺序
装饰器的执行顺序分为两个阶段,理解这一点非常重要:
1️⃣ 工厂函数执行阶段(从上到下,从外到内)
typescript
function A() {
console.log('1. 工厂 A 被调用');
return function (target: any) {
console.log('2. 装饰器 A 被执行');
};
}
function B() {
console.log('3. 工厂 B 被调用');
return function (target: any) {
console.log('4. 装饰器 B 被执行');
};
}
function C() {
console.log('5. 工厂 C 被调用');
return function (target: any) {
console.log('6. 装饰器 C 被执行');
};
}
@A()
@B()
@C()
class Example {}输出:
1. 工厂 A 被调用
3. 工厂 B 被调用
5. 工厂 C 被调用
6. 装饰器 C 被执行
4. 装饰器 B 被执行
2. 装饰器 A 被执行规律:
- 工厂函数:从上到下执行(1→3→5)
- 装饰器函数:从下到上执行(6→4→2)
2️⃣ 类成员装饰器的完整执行顺序
typescript
function ClassDec() {
console.log('① 类装饰器工厂');
return function (target: any) {
console.log('⑥ 类装饰器执行');
};
}
function PropDec() {
console.log('② 属性装饰器工厂');
return function (target: any, key: string) {
console.log('④ 属性装饰器执行');
};
}
function MethodDec() {
console.log('③ 方法装饰器工厂');
return function (target: any, key: string, descriptor: PropertyDescriptor) {
console.log('⑤ 方法装饰器执行');
};
}
@ClassDec()
class Example {
@PropDec()
name: string;
@MethodDec()
method() {}
}完整执行顺序:
① 类装饰器工厂(最外层)
② 属性装饰器工厂
③ 方法装饰器工厂
④ 属性装饰器执行
⑤ 方法装饰器执行
⑥ 类装饰器执行(最内层)3️⃣ 同一声明上的多个装饰器
typescript
function First() {
console.log('工厂 First');
return function (target: any, key: string, descriptor: PropertyDescriptor) {
console.log('装饰器 First 执行');
};
}
function Second() {
console.log('工厂 Second');
return function (target: any, key: string, descriptor: PropertyDescriptor) {
console.log('装饰器 Second 执行');
};
}
function Third() {
console.log('工厂 Third');
return function (target: any, key: string, descriptor: PropertyDescriptor) {
console.log('装饰器 Third 执行');
};
}
class Example {
@First()
@Second()
@Third()
method() {}
}输出:
工厂 First (从上到下)
工厂 Second
工厂 Third
装饰器 Third 执行 (从下到上)
装饰器 Second 执行
装饰器 First 执行实际效果:
typescript
// 相当于:
method = First(Second(Third(
Object.getOwnPropertyDescriptor(Example.prototype, 'method')
)));4️⃣ 类成员的声明顺序
typescript
function logOrder(name: string) {
return function (target: any, key: string) {
console.log(`${name}: ${key}`);
};
}
class Example {
@logOrder('属性1')
a: string;
@logOrder('属性2')
b: number;
@logOrder('方法1')
method1() {}
@logOrder('方法2')
method2() {}
}输出:
属性1: a
属性2: b
方法1: method1
方法2: method2规律: 按照声明顺序从上到下执行
📊 执行顺序总结表
| 阶段 | 顺序 | 说明 |
|---|---|---|
| 工厂函数调用 | ⬇️ 从上到下 | 最先执行,返回真正的装饰器函数 |
| 成员声明顺序 | ⬇️ 从上到下 | 属性、方法按代码声明顺序 |
| 装饰器函数应用 | ⬆️ 从下到上 | 后声明的先应用(类似洋葱模型) |
| 实际调用时 | ⬆️ 从外到内 | 运行时最外层装饰器先执行 |
🎯 实用记忆口诀
工厂从上走到下
装饰从下往上挂
方法调用先外层
层层包裹像穿褂⚠️ 注意事项
- 静态成员的装饰器先于实例成员执行
- 参数装饰器在方法装饰器之前执行
- 装饰器的执行顺序影响功能,特别是日志、验证等场景
属性装饰器
基本语法
typescript
function PropertyDecorator(target: any, key: string) {
// target: 实例属性为原型对象,静态属性为构造函数
// key: 属性名字符串
}实战:依赖注入
typescript
import "reflect-metadata";
function Inject(target: any, key: string) {
const type = Reflect.getMetadata("design:type", target, key);
target[key] = new type();
}
class Logger {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
class UserService {
@Inject
logger!: Logger; // ! 表示"我会确保这个属性被赋值"
createUser() {
this.logger.log("用户创建成功");
}
}
new UserService().createUser();
// 输出: [LOG] 用户创建成功工作原理
TypeScript 编译器会为装饰的属性自动存储类型元数据:
typescript
class UserService {
@Inject
logger!: Logger;
}
// 编译后等同于:
class UserService {
logger!: Logger;
}
// 自动添加:Reflect.metadata("design:type", Logger)(UserService.prototype, "logger");
// 然后调用:Inject(UserService.prototype, "logger");确定赋值断言
typescript
logger!: Logger; // 告诉编译器:"我保证在使用前会被赋值"为什么需要? TypeScript 默认要求属性要么初始化,要么标记为可选。使用 ! 可以避免这两个限制。
实战:属性验证
typescript
function Validate(rule: { minLength?: number; pattern?: RegExp }) {
return function (target: any, key: string) {
let value: any;
Object.defineProperty(target, key, {
get: () => value,
set: (newValue: any) => {
if (rule.minLength && newValue?.length < rule.minLength) {
throw new Error(`${key} 长度不能少于 ${rule.minLength}`);
}
if (rule.pattern && !rule.pattern.test(newValue)) {
throw new Error(`${key} 格式不正确`);
}
value = newValue;
},
});
};
}
class User {
@Validate({ minLength: 3, pattern: /^[a-zA-Z]+$/ })
name!: string;
}
const user = new User();
user.name = "Jo"; // ❌ 抛出错误: name 长度不能少于 3方法装饰器
基本语法
typescript
function MethodDecorator(
target: any, // 实例方法:原型对象;静态方法:构造函数
key: string, // 方法名
descriptor: PropertyDescriptor, // 方法描述符
) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
// 修改方法行为
return original.apply(this, args);
};
}常用案例
1. 日志装饰器
typescript
function Log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[LOG] ${key} 被调用,参数:`, args);
const result = original.apply(this, args);
console.log(`[LOG] ${key} 返回:`, result);
return result;
};
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
}
new Calculator().add(1, 2);
// [LOG] add 被调用,参数: [1, 2]
// [LOG] add 返回: 32. 防抖装饰器
typescript
function Debounce(delay: number) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
let timer: NodeJS.Timeout | null = null;
descriptor.value = function (...args: any[]) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => original.apply(this, args), delay);
};
};
}
class SearchBox {
query = "";
@Debounce(300)
search() {
console.log("搜索:", this.query);
}
}3. 缓存装饰器
typescript
function Cache(ttl?: number) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
const cache = new Map<string, { value: any; expiry: number }>();
descriptor.value = function (...args: any[]) {
const cacheKey = JSON.stringify(args);
const cached = cache.get(cacheKey);
if (cached && (!ttl || Date.now() < cached.expiry)) {
console.log("从缓存返回");
return cached.value;
}
const result = original.apply(this, args);
cache.set(cacheKey, {
value: result,
expiry: ttl ? Date.now() + ttl : Number.MAX_VALUE,
});
return result;
};
};
}
class MathService {
@Cache(5000) // 缓存5秒
fibonacci(n: number): number {
console.log("计算 fibonacci");
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}4. 重试装饰器
typescript
function Retry(maxAttempts: number = 3, delay: number = 1000) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = async function (...args: any[]) {
let lastError: any;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await original.apply(this, args);
} catch (error) {
lastError = error;
if (attempt < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw new Error(`${key} 失败 ${maxAttempts} 次后放弃`);
};
};
}
class ApiService {
@Retry(3, 1000)
async fetchData(url: string) {
const response = await fetch(url);
if (!response.ok) throw new Error("请求失败");
return response.json();
}
}装饰器工厂
装饰器工厂是一个返回装饰器函数的函数,用于传递自定义参数。
typescript
// 装饰器工厂
function Debounce(delay: number) {
// 返回真正的装饰器
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
let timer: NodeJS.Timeout | null = null;
descriptor.value = function (...args: any[]) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => original.apply(this, args), delay);
};
};
}
// 使用
class SearchBox {
@Debounce(300) // 传递参数
search() {}
}链式装饰器
多个装饰器可以组合使用,从下到上依次执行:
typescript
class UserService {
@Log()
@Validate({ name: "string" })
@Catch((error) => console.error(error))
createUser(name: string) {
// 先执行 Catch,再 Validate,最后 Log
}
}实际应用场景
1. NestJS 风格的依赖注入
typescript
import "reflect-metadata";
const Injectable = () => (_target: any) => {};
const constructors = new Map<string, any>();
function Inject(token?: string) {
return function (target: any, key: string) {
const type = Reflect.getMetadata("design:type", target, key);
const dependencyToken = token || type.name;
Object.defineProperty(target, key, {
get() {
if (!constructors.has(dependencyToken)) {
throw new Error(`依赖 ${dependencyToken} 未注册`);
}
return constructors.get(dependencyToken);
},
set(value) {
constructors.set(dependencyToken, value);
},
});
};
}
@Injectable()
class Logger {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
@Injectable()
class Database {
connect() {
console.log("[DB] 连接数据库");
}
}
class UserService {
@Inject()
logger!: Logger;
@Inject()
db!: Database;
createUser() {
this.logger.log("创建用户");
this.db.connect();
}
}
// 注册依赖
constructors.set("Logger", new Logger());
constructors.set("Database", new Database());
new UserService().createUser();
// [LOG] 创建用户
// [DB] 连接数据库2. API 路由装饰器
typescript
const ROUTES: Array<{
path: string;
method: string;
handler: Function;
}> = [];
function Controller(prefix: string) {
return function <T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
constructor(...args: any[]) {
super(...args);
console.log(`📦 控制器注册: ${prefix}`);
}
};
};
}
function Get(path: string) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
ROUTES.push({
path,
method: "GET",
handler: descriptor.value,
});
};
}
function Post(path: string) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
ROUTES.push({
path,
method: "POST",
handler: descriptor.value,
});
};
}
@Controller("/api/users")
class UserController {
@Get("/:id")
getUser(id: string) {
return { id, name: "用户详情" };
}
@Post("/")
createUser(data: any) {
return { success: true, data };
}
}
console.log("📋 已注册的路由:", ROUTES);
// 📦 控制器注册: /api/users
// 📋 已注册的路由: [
// { path: '/:id', method: 'GET', handler: [Function: getUser] },
// { path: '/', method: 'POST', handler: [Function: createUser] }
// ]3. ORM 风格的实体定义
typescript
interface ColumnMetadata {
type: "string" | "number" | "boolean" | "date";
primary?: boolean;
nullable?: boolean;
length?: number;
}
const entityMetadata = new Map<Function, Map<string, ColumnMetadata>>();
function Entity(tableName: string) {
return function (constructor: Function) {
console.log(`📊 实体表名: ${tableName}`);
};
}
function Column(options: ColumnMetadata) {
return function (target: any, key: string) {
if (!entityMetadata.has(target.constructor)) {
entityMetadata.set(target.constructor, new Map());
}
entityMetadata.get(target.constructor)!.set(key, options);
};
}
@Entity("users")
class User {
@Column({ type: "number", primary: true })
id!: number;
@Column({ type: "string", length: 100 })
name!: string;
@Column({ type: "string", length: 255, nullable: true })
email!: string | null;
@Column({ type: "boolean", nullable: false })
isActive!: boolean;
}
// 查看元数据
const userMetadata = entityMetadata.get(User);
console.log("📋 User 实体元数据:", userMetadata);
// 📊 实体表名: users
// 📋 User 实体元数据: Map {
// 'id' => { type: 'number', primary: true },
// 'name' => { type: 'string', length: 100 },
// 'email' => { type: 'string', length: 255, nullable: true },
// 'isActive' => { type: 'boolean', nullable: false }
// }4. 表单验证
typescript
interface ValidationRule {
required?: boolean;
minLength?: number;
maxLength?: number;
pattern?: RegExp;
custom?: (value: any) => boolean | string;
}
const validations = new Map<Object, Map<string, ValidationRule[]>>();
function Validate(rules: ValidationRule | ValidationRule[]) {
return function (target: any, key: string) {
const ruleArray = Array.isArray(rules) ? rules : [rules];
if (!validations.has(target)) {
validations.set(target, new Map());
}
validations.get(target)!.set(key, ruleArray);
let value: any;
return {
get() {
return value;
},
set(newValue: any) {
const errors: string[] = [];
for (const rule of ruleArray) {
if (rule.required && !newValue) {
errors.push(`${key} 是必填项`);
}
if (rule.minLength && newValue?.length < rule.minLength) {
errors.push(`${key} 长度不能少于 ${rule.minLength}`);
}
if (rule.maxLength && newValue?.length > rule.maxLength) {
errors.push(`${key} 长度不能超过 ${rule.maxLength}`);
}
if (rule.pattern && !rule.pattern.test(newValue)) {
errors.push(`${key} 格式不正确`);
}
if (rule.custom) {
const result = rule.custom(newValue);
if (result !== true) {
errors.push(result as string);
}
}
}
if (errors.length > 0) {
console.error(`❌ 验证失败 (${key}):`, errors.join(", "));
throw new Error(errors.join(", "));
}
value = newValue;
},
};
};
}
class RegisterForm {
@Validate({
required: true,
minLength: 3,
maxLength: 20,
pattern: /^[a-zA-Z0-9_]+$/,
})
username!: string;
@Validate({
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
})
email!: string;
@Validate([
{ required: true, minLength: 8 },
{
custom: (value) => /[A-Z]/.test(value) || "密码必须包含大写字母",
},
{
custom: (value) => /[a-z]/.test(value) || "密码必须包含小写字母",
},
{
custom: (value) => /[0-9]/.test(value) || "密码必须包含数字",
},
])
password!: string;
}
const form = new RegisterForm();
form.username = "user_123"; // ✅
form.email = "user@test.com"; // ✅
form.password = "Password123"; // ✅
// form.username = 'ab'; // ❌ username 长度不能少于 3
// form.email = 'invalid'; // ❌ email 格式不正确
// form.password = 'weak'; // ❌ 密码必须包含大写字母、小写字母、数字最佳实践
✅ 应该做的
使用装饰器工厂提供参数
typescriptfunction Log(prefix: string) { return function ( target: any, key: string, descriptor: PropertyDescriptor, ) { // ... }; }保持装饰器简单专注
typescript// ✅ 职责单一 @Log @Validate method() {} // ❌ 职责混乱 @LogAndValidateAndCache method() {}支持装饰器组合
typescriptfunction Compose(...decorators: PropertyDecorator[]) { return function ( target: any, key: string, descriptor: PropertyDescriptor, ) { decorators.forEach((decorator) => decorator(target, key, descriptor)); }; }提供类型安全
typescriptfunction TypedDecorator<T>(config: T) { return function (target: any, key: string) { // 使用 config 的类型信息 }; }文档化装饰器行为
typescript/** * 防抖装饰器 * @param delay 延迟时间(毫秒) * @example * class Example { * @Debounce(300) * handleInput() {} * } */ function Debounce(delay: number) {}
❌ 不应该做的
不要在装饰器中执行副作用
typescript// ❌ 不好:装饰器执行副作用 function BadDecorator(target: any, key: string) { console.log("执行了副作用"); fetch("/api/log").then(/* ... */); } // ✅ 好:延迟副作用到方法调用时 function GoodDecorator( target: any, key: string, descriptor: PropertyDescriptor, ) { const original = descriptor.value; descriptor.value = function (...args: any[]) { console.log("方法调用时执行副作用"); return original.apply(this, args); }; }不要修改装饰器目标的结构
typescript// ❌ 危险:修改原型链 function BadDecorator(target: any) { target.prototype.newMethod = () => {}; } // ✅ 安全:只修改描述符 function GoodDecorator( target: any, key: string, descriptor: PropertyDescriptor, ) { descriptor.writable = false; }不要忘记保持
this绑定typescript// ❌ 错误:丢失 this 绑定 descriptor.value = function (...args: any[]) { return original(...args); // this 指向错误 }; // ✅ 正确:保持 this 绑定 descriptor.value = function (...args: any[]) { return original.apply(this, args); };不要忽略错误处理
typescript// ✅ 好的做法 function SafeDecorator( target: any, key: string, descriptor: PropertyDescriptor, ) { const original = descriptor.value; descriptor.value = async function (...args: any[]) { try { return await original.apply(this, args); } catch (error) { console.error(`装饰器错误: ${key}`, error); throw error; } }; }
总结
关键要点
- 装饰器是编译时的元编程工具,用于声明式地修改类、方法、属性的行为
- Reflect Metadata 是装饰器的核心依赖,提供了运行时类型反射能力
- 装饰器工厂是传递参数的标准方式,使装饰器更加灵活可配置
- 组合优于配置,多个简单的装饰器比一个复杂的装饰器更好
🎯 掌握装饰器,让你的代码更优雅、更可维护!