TypeScript装饰器与元编程实战
作者:专注前端开发,分享工程化实战经验
更新时间:2026年5月
阅读时长:约15分钟
前言:为什么装饰器是TypeScript的杀手锏?
如果你使用过Angular或NestJS一定会注意到:它们的代码充满了@Injectable()、@Component()、@Controller()这样的"魔法标记"。这就是**装饰器(Decorators)**的魔力。
装饰器让TypeScript具备了元编程能力------用声明式的方式为类、方法、属性添加额外行为。本文将带你从入门到实战,彻底掌握这一强大特性。
一、装饰器基础入门
1.1 什么是装饰器?
装饰器是一个函数,它能在不修改原始类的前提下,给类或类的成员添加额外功能:
typescript
// 简单装饰器
function Logger(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with`, args);
return originalMethod.apply(this, args);
};
return descriptor;
}
class Calculator {
@Logger
add(a: number, b: number) {
return a + b;
}
}
const calc = new Calculator();
calc.add(1, 2);
// 输出: Calling add with [ 1, 2 ]
// 返回: 3
1.2 装饰器的四种类型
typescript
// 1. 类装饰器
function Controller(path: string) {
return function (target: new () => any) {
console.log(`Registered controller at /${path}`);
};
}
// 2. 方法装饰器
function Get(path: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
// 注册路由逻辑
};
}
// 3. 属性装饰器
function Autowired(key: string) {
return function (target: any, propertyKey: string) {
// 依赖注入逻辑
};
}
// 4. 参数装饰器
function Param(source: string) {
return function (target: any, propertyKey: string, index: number) {
// 参数处理逻辑
};
}
二、类装饰器实战
2.1 路由控制器装饰器
模拟NestJS的路由装饰器:
typescript
interface RouteHandler {
path: string;
method: string;
handler: Function;
}
function Controller(basePath: string) {
return function <T extends new (...args: any[]) => any>(target: T) {
return class extends target {
private routes: RouteHandler[] = [];
constructor(...args: any[]) {
super(...args);
// 注册所有路由
this.registerRoutes();
}
private registerRoutes() {
// 收集元数据
// ...
console.log(`Controller ${target.name} mounted at /${basePath}`);
}
};
};
}
function Get(path: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`GET /${path} handled by ${propertyKey}`);
return original.apply(this, args);
};
return descriptor;
};
}
// 使用
@Controller("users")
class UserController {
@Get("list")
findAll() {
return [{ id: 1, name: "张三" }];
}
@Get(":id")
findById(id: number) {
return { id, name: "张三" };
}
}
2.2 单例模式装饰器
typescript
function Singleton(target: new () => any) {
let instance: any = null;
return new Proxy(target, {
construct: function (cls, args) {
if (!instance) {
instance = new cls(...args);
}
return instance;
}
});
}
@Singleton
class ConfigService {
private config = { apiUrl: "https://api.example.com" };
get(key: string) {
return (this.config as any)[key];
}
}
const a = new ConfigService();
const b = new ConfigService();
console.log(a === b); // true
三、方法装饰器进阶
3.1 缓存装饰器
typescript
function Cache(ttl: number = 60000) {
const cache = new Map<string, { value: any; expires: number }>();
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
const key = `${propertyKey}:${JSON.stringify(args)}`;
const cached = cache.get(key);
if (cached && cached.expires > Date.now()) {
console.log(`Cache hit for ${propertyKey}`);
return cached.value;
}
const result = original.apply(this, args);
// 处理Promise
if (result instanceof Promise) {
return result.then((value: any) => {
cache.set(key, { value, expires: Date.now() + ttl });
return value;
});
}
cache.set(key, { value: result, expires: Date.now() + ttl });
return result;
};
return descriptor;
};
}
class UserService {
@Cache(5000) // 缓存5秒
async getUser(id: number) {
await new Promise(r => setTimeout(r, 100));
return { id, name: "用户" + id };
}
}
3.2 重试装饰器
typescript
function Retry(
maxAttempts: number = 3,
delay: number = 1000
) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = async function (...args: any[]) {
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await original.apply(this, args);
} catch (error) {
lastError = error as Error;
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
if (attempt < maxAttempts) {
await new Promise(r => setTimeout(r, delay));
}
}
}
throw lastError!;
};
return descriptor;
};
}
class ApiClient {
@Retry(3, 500)
async fetch(url: string) {
if (Math.random() > 0.7) {
throw new Error("Network error");
}
return { data: "success" };
}
}
四、属性装饰器与依赖注入
4.1 简单的依赖注入容器
typescript
type Constructor<T = any> = new (...args: any[]) => T;
// 服务容器
const Container = new Map<Constructor, Constructor>();
function Injectable(token?: string) {
return function (target: Constructor) {
const t = token || target;
Container.set(t, target);
console.log(`Registered service: ${target.name}`);
};
}
function Inject(token: string) {
return function (target: any, propertyKey: string) {
// 替换属性为注入的服务实例
Object.defineProperty(target, propertyKey, {
get: () => {
const ServiceClass = Container.get(token as any);
return new ServiceClass();
}
});
};
}
// 使用
@Injectable("UserService")
class UserService {
getUsers() {
return ["张三", "李四"];
}
}
class UserController {
@Inject("UserService")
private userService!: UserService;
list() {
return this.userService.getUsers();
}
}
五、装饰器工厂与组合
5.1 日志中间件装饰器
typescript
function Log() {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const isAsync = descriptor.value.constructor.name === "AsyncFunction";
if (isAsync) {
const original = descriptor.value;
descriptor.value = async function (...args: any[]) {
const start = Date.now();
console.log(`[START] ${target.constructor.name}.${propertyKey}`);
try {
const result = await original.apply(this, args);
console.log(`[END] ${propertyKey} took ${Date.now() - start}ms`);
return result;
} catch (error) {
console.log(`[ERROR] ${propertyKey}:`, error);
throw error;
}
};
} else {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey}`, args);
return original.apply(this, args);
};
}
return descriptor;
};
}
// 使用
class OrderService {
@Log()
async createOrder(order: any) {
await new Promise(r => setTimeout(r, 100));
return { id: 1, ...order };
}
@Log()
calculate(itemCount: number, price: number) {
return itemCount * price;
}
}
5.2 权限校验装饰器
typescript
interface Role {
name: string;
level: number;
}
const roleLevels: Record<string, number> = {
guest: 0,
user: 1,
admin: 2,
superadmin: 3
};
function Require(levelOrRole: number | string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const requiredLevel = typeof levelOrRole === "number"
? levelOrRole
: roleLevels[levelOrRole];
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
const currentUser = (globalThis as any).currentUser;
// 假设从上下文获取用户级别
const userLevel = currentUser?.role?.level ?? 0;
if (userLevel < requiredLevel) {
throw new Error(`需要权限级别 ${requiredLevel},当前 ${userLevel}`);
}
return original.apply(this, args);
};
return descriptor;
};
}
// 使用
class AdminPanel {
@Require(2)
deleteUser(id: number) {
console.log(`Deleted user ${id}`);
}
@Require("admin")
modifySettings(settings: any) {
console.log("Modified settings");
}
}
六、元数据反射
6.1 使用 reflect-metadata
typescript
import "reflect-metadata";
const METADATA_KEY = "design:paramtypes";
function Controller(path: string) {
return function (target: new () => any) {
// 在类上存储路由路径
Reflect.defineMetadata("route:path", path, target);
// 可以存储更多元数据
Reflect.defineMetadata("route:middleware", [], target);
return target;
};
}
function Get(path: string) {
return function (target: any, propertyKey: string) {
// 存储方法路径
Reflect.defineMetadata("route:handler", { path, method: "GET" }, target, propertyKey);
// 存储参数类型
const paramTypes = Reflect.getMetadata(METADATA_KEY, target, propertyKey);
Reflect.defineMetadata("route:paramTypes", paramTypes, target, propertyKey);
};
}
// 读取元数据
function getRouteMetadata(target: new () => any) {
return {
path: Reflect.getMetadata("route:path", target),
handlers: Object.getOwnPropertyNames(target.prototype)
.filter(k => k !== "constructor")
.map(k => ({
method: k,
config: Reflect.getMetadata("route:handler", target.prototype, k)
}))
};
}
七、总结与实践建议
| 装饰器类型 | ���用���景 | 经典案例 |
|---|---|---|
| 类装饰器 | 依赖注入、单例、AOP | NestJS、Angular |
| 方法装饰器 | 缓存、重试、事务 | 数据访问层 |
| 参数装饰器 | 参数校验、转换 | API 入参处理 |
| 属性装饰器 | 依赖注入、自动装配 | IoC 容器 |
最佳实践
- 不要过度装饰:每个装饰器都有开销,保持合理的抽象层级
- 类型安全:充分利用TypeScript的类型系统
- 调试友好:装饰器会增加调用栈,添加有意义的日志
- 组合优于继承:用装饰器组合代替类继承
互动讨论
你在项目中使用装饰器了吗?遇到过什么问题?