TypeScript装饰器与元编程实战
作者:专注前端开发,分享工程化实战经验
更新时间:2026年5月
阅读时长:约15分钟
前言:为什么装饰器是TypeScript的杀手锏?
如果你使用过Angular或NestJS一定会注意到:它们的代码充满了@Injectable()、@Component()、@Controller()这样的"魔法标记"。这就是**装饰器(Decorators)**的魔力。
装饰器让TypeScript具备了元编程能力——用声明式的方式为类、方法、属性添加额外行为。本文将带你从入门到实战,彻底掌握这一强大特性。
一、装饰器基础入门
1.1 什么是装饰器?
装饰器是一个函数,它能在不修改原始类的前提下,给类或类的成员添加额外功能:
// 简单装饰器functionLogger(target:any,propertyKey:string,descriptor:PropertyDescriptor){constoriginalMethod=descriptor.value;descriptor.value=function(...args:any[]){console.log(`Calling${propertyKey}with`,args);returnoriginalMethod.apply(this,args);};returndescriptor;}classCalculator{@Loggeradd(a:number,b:number){returna+b;}}constcalc=newCalculator();calc.add(1,2);// 输出: Calling add with [ 1, 2 ]// 返回: 31.2 装饰器的四种类型
// 1. 类装饰器functionController(path:string){returnfunction(target:new()=>any){console.log(`Registered controller at /${path}`);};}// 2. 方法装饰器functionGet(path:string){returnfunction(target:any,propertyKey:string,descriptor:PropertyDescriptor){// 注册路由逻辑};}// 3. 属性装饰器functionAutowired(key:string){returnfunction(target:any,propertyKey:string){// 依赖注入逻辑};}// 4. 参数装饰器functionParam(source:string){returnfunction(target:any,propertyKey:string,index:number){// 参数处理逻辑};}二、类装饰器实战
2.1 路由控制器装饰器
模拟NestJS的路由装饰器:
interfaceRouteHandler{path:string;method:string;handler:Function;}functionController(basePath:string){returnfunction<Textendsnew(...args:any[])=>any>(target:T){returnclassextendstarget{privateroutes:RouteHandler[]=[];constructor(...args:any[]){super(...args);// 注册所有路由this.registerRoutes();}privateregisterRoutes(){// 收集元数据// ...console.log(`Controller${target.name}mounted at /${basePath}`);}};};}functionGet(path:string){returnfunction(target:any,propertyKey:string,descriptor:PropertyDescriptor){constoriginal=descriptor.value;descriptor.value=function(...args:any[]){console.log(`GET /${path}handled by${propertyKey}`);returnoriginal.apply(this,args);};returndescriptor;};}// 使用@Controller("users")classUserController{@Get("list")findAll(){return[{id:1,name:"张三"}];}@Get(":id")findById(id:number){return{id,name:"张三"};}}2.2 单例模式装饰器
functionSingleton(target:new()=>any){letinstance:any=null;returnnewProxy(target,{construct:function(cls,args){if(!instance){instance=newcls(...args);}returninstance;}});}@SingletonclassConfigService{privateconfig={apiUrl:"https://api.example.com"};get(key:string){return(this.configasany)[key];}}consta=newConfigService();constb=newConfigService();console.log(a===b);// true三、方法装饰器进阶
3.1 缓存装饰器
functionCache(ttl:number=60000){constcache=newMap<string,{value:any;expires:number}>();returnfunction(target:any,propertyKey:string,descriptor:PropertyDescriptor){constoriginal=descriptor.value;descriptor.value=function(...args:any[]){constkey=`${propertyKey}:${JSON.stringify(args)}`;constcached=cache.get(key);if(cached&&cached.expires>Date.now()){console.log(`Cache hit for${propertyKey}`);returncached.value;}constresult=original.apply(this,args);// 处理Promiseif(resultinstanceofPromise){returnresult.then((value:any)=>{cache.set(key,{value,expires:Date.now()+ttl});returnvalue;});}cache.set(key,{value:result,expires:Date.now()+ttl});returnresult;};returndescriptor;};}classUserService{@Cache(5000)// 缓存5秒asyncgetUser(id:number){awaitnewPromise(r=>setTimeout(r,100));return{id,name:"用户"+id};}}3.2 重试装饰器
functionRetry(maxAttempts:number=3,delay:number=1000){returnfunction(target:any,propertyKey:string,descriptor:PropertyDescriptor){constoriginal=descriptor.value;descriptor.value=asyncfunction(...args:any[]){letlastError:Error;for(letattempt=1;attempt<=maxAttempts;attempt++){try{returnawaitoriginal.apply(this,args);}catch(error){lastError=errorasError;console.log(`Attempt${attempt}failed, retrying in${delay}ms...`);if(attempt<maxAttempts){awaitnewPromise(r=>setTimeout(r,delay));}}}throwlastError!;};returndescriptor;};}classApiClient{@Retry(3,500)asyncfetch(url:string){if(Math.random()>0.7){thrownewError("Network error");}return{data:"success"};}}四、属性装饰器与依赖注入
4.1 简单的依赖注入容器
typeConstructor<T=any>=new(...args:any[])=>T;// 服务容器constContainer=newMap<Constructor,Constructor>();functionInjectable(token?:string){returnfunction(target:Constructor){constt=token||target;Container.set(t,target);console.log(`Registered service:${target.name}`);};}functionInject(token:string){returnfunction(target:any,propertyKey:string){// 替换属性为注入的服务实例Object.defineProperty(target,propertyKey,{get:()=>{constServiceClass=Container.get(tokenasany);returnnewServiceClass();}});};}// 使用@Injectable("UserService")classUserService{getUsers(){return["张三","李四"];}}classUserController{@Inject("UserService")privateuserService!:UserService;list(){returnthis.userService.getUsers();}}五、装饰器工厂与组合
5.1 日志中间件装饰器
functionLog(){returnfunction(target:any,propertyKey:string,descriptor:PropertyDescriptor){constisAsync=descriptor.value.constructor.name==="AsyncFunction";if(isAsync){constoriginal=descriptor.value;descriptor.value=asyncfunction(...args:any[]){conststart=Date.now();console.log(`[START]${target.constructor.name}.${propertyKey}`);try{constresult=awaitoriginal.apply(this,args);console.log(`[END]${propertyKey}took${Date.now()-start}ms`);returnresult;}catch(error){console.log(`[ERROR]${propertyKey}:`,error);throwerror;}};}else{constoriginal=descriptor.value;descriptor.value=function(...args:any[]){console.log(`Calling${propertyKey}`,args);returnoriginal.apply(this,args);};}returndescriptor;};}// 使用classOrderService{@Log()asynccreateOrder(order:any){awaitnewPromise(r=>setTimeout(r,100));return{id:1,...order};}@Log()calculate(itemCount:number,price:number){returnitemCount*price;}}5.2 权限校验装饰器
interfaceRole{name:string;level:number;}constroleLevels:Record<string,number>={guest:0,user:1,admin:2,superadmin:3};functionRequire(levelOrRole:number|string){returnfunction(target:any,propertyKey:string,descriptor:PropertyDescriptor){constrequiredLevel=typeoflevelOrRole==="number"?levelOrRole:roleLevels[levelOrRole];constoriginal=descriptor.value;descriptor.value=function(...args:any[]){constcurrentUser=(globalThisasany).currentUser;// 假设从上下文获取用户级别constuserLevel=currentUser?.role?.level??0;if(userLevel<requiredLevel){thrownewError(`需要权限级别${requiredLevel},当前${userLevel}`);}returnoriginal.apply(this,args);};returndescriptor;};}// 使用classAdminPanel{@Require(2)deleteUser(id:number){console.log(`Deleted user${id}`);}@Require("admin")modifySettings(settings:any){console.log("Modified settings");}}六、元数据反射
6.1 使用 reflect-metadata
import"reflect-metadata";constMETADATA_KEY="design:paramtypes";functionController(path:string){returnfunction(target:new()=>any){// 在类上存储路由路径Reflect.defineMetadata("route:path",path,target);// 可以存储更多元数据Reflect.defineMetadata("route:middleware",[],target);returntarget;};}functionGet(path:string){returnfunction(target:any,propertyKey:string){// 存储方法路径Reflect.defineMetadata("route:handler",{path,method:"GET"},target,propertyKey);// 存储参数类型constparamTypes=Reflect.getMetadata(METADATA_KEY,target,propertyKey);Reflect.defineMetadata("route:paramTypes",paramTypes,target,propertyKey);};}// 读取元数据functiongetRouteMetadata(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的类型系统
- 调试友好:装饰器会增加调用栈,添加有意义的日志
- 组合优于继承:用装饰器组合代替类继承
互动讨论
你在项目中使用装饰器了吗?遇到过什么问题?