TypeScript 装饰器实用指南!

虽然 JavaScript 目前还没有装饰器的概念(ES6 类的装饰器提案[1]目前处于第三阶段,还未完成),但 TypeScript 5.0 中已经引入了装饰器,本文将了解什么是装饰器以及如何在 TypeScript 中使用装饰器!
首页 新闻资讯 行业资讯 TypeScript 装饰器实用指南!

f3fcfd508436a189b9b4154248dbe6d5476ae9.png

一、装饰器的概念 Summer IS HERE

在 TypeScript 中,装饰器就是可以添加到类及其成员的函数。TypeScript 装饰器可以注释和修改类声明、方法、属性和访问器。Decorator类型定义如下:

typeDecorator=(target:Input,context:{kind:string;name:string|symbol;access:{get?():unknown;set?(value:unknown):void;};private?:boolean;static?:boolean;addInitializer?(initializer:()=>void):void;})=>Output|void;

上面的类型定义解释如下:

  • target:代表要装饰的元素,其类型为 Input。

  • context 包含有关如何声明修饰方法的元数据,即:

  • kind:装饰值的类型。正如我们将看到的,这可以是类、方法、getter、setter、字段或访问器。

  • name:被装饰对象的名称。

  • access:引用 getter 和 setter 方法来访问装饰对象的对象。

  • private:被装饰的对象是否是私有类成员。

  • static:被修饰的对象是否是静态类成员。

  • addInitializer:一种在构造函数开头(或定义类时)添加自定义初始化逻辑的方法。

  • Output:表示 Decorator 函数返回值的类型。

二、装饰器的类型 Summer IS HERE

接下来,我们就来了解一下装饰器的各种类型。

Summer:类装饰器

当将函数作为装饰器附加到类时,将收到类构造函数作为第一个参数:

type ClassDecorator=(value:Function,context:{kind:"class"name:string|undefinedaddInitializer(initializer:()=>void):void})=>Function|void

例如,假设想要使用装饰器向 Rocket 类添加两个属性:fuel 和 isEmpty()。在这种情况下,可以编写以下函数:

functionWithFuel(target:typeofRocket,context):typeofRocket{if(context.kind==="class"){returnclassextendstarget{fuel:number=50isEmpty():boolean{returnthis.fuel==0}}}}

在确保装饰元素的类型确实是类之后,返回一个具有两个附加属性的新类。或者,可以使用原型对象来动态添加新方法:

functionWithFuel(target:typeofRocket,context):typeofRocket{if(context.kind==="class"){target.prototype.fuel=50target.prototype.isEmpty=():boolean=>{returnthis.fuel==0}}}

可以按以下方式使用 WithFuel:

@WithFuelclassRocket{}constrocket=newRocket()console.log((rocketasany).fuel)console.log(`empty?${(rocketasany).isEmpty()}`)/* Prints:
50
empty? false
*/

可以看到,这里将rocket转换为any类型才能访问新的属性。这是因为装饰器无法影响类型的结构。

如果原始类定义了一个稍后被装饰的属性,装饰器会覆盖原始值。例如,如果Rocket有一个具有不同值的fuel属性,WithFuel装饰器将会覆盖该值:

functionWithFuel(target:typeofRocket,context):typeofRocket{if(context.kind==="class"){returnclassextendstarget{fuel:number=50isEmpty():boolean{returnthis.fuel==0}}}}@WithFuelclassRocket{fuel:number=75}constrocket=newRocket()console.log((rocketasany).fuel)// 50

Summer:方法装饰器

方法装饰器可以用于装饰类方法。在这种情况下,装饰器函数的类型如下:

type ClassMethodDecorator=(target:Function,context:{kind:"method"name:string|symbolaccess:{get():unknown}static:booleanprivate:booleanaddInitializer(initializer:()=>void):void})=>Function|void

如果希望在调用被装饰的方法之前或之后执行某些操作时,就可以使用方法装饰器。

例如,在开发过程中,记录对特定方法的调用或在调用之前/之后验证前置/后置条件可能非常有用。此外,我们还可以影响方法的调用方式,例如通过延迟其执行或限制在一定时间内的调用次数。

最后,可以使用方法装饰器将一个方法标记为已废弃,并记录一条消息来警告用户,并告知他们应该使用哪个方法代替:

functiondeprecatedMethod(target:Function,context){if(context.kind==="method"){returnfunction(...args:any[]){console.log(`${context.name}is deprecated and will be removed in a future version.`)returntarget.apply(this,args)}}}

在这种情况下,deprecatedMethod函数的第一个参数是要装饰的方法。确认它确实是一个方法后(context.kind === "method"),返回一个新的函数,该函数在调用实际方法之前包装被装饰的方法并记录一条警告消息。

接下来,可以按照以下方式使用装饰器:

@WithFuelclassRocket{fuel:number=75@deprecatedMethodisReadyForLaunch():Boolean{return!(thisasany).isEmpty()}}constrocket=newRocket()console.log(`Is ready for launch?${rocket.isReadyForLaunch()}`)

在isReadyForLaunch()方法中,引用了通过WithFuel装饰器添加的isEmpty方法。注意,必须将其转换为any类型的实例,与之前一样。当调用isReadyForLaunch()方法时,会看到以下输出,显示警告消息被正确地打印出来:

isReadyForLaunch is deprecated and will be removedina future version.Is the readyforlaunch?true

Summer:属性装饰器

属性装饰器与方法装饰器的类型非常相似:

type ClassPropertyDecorator=(target:undefined,context:{kind:"field"name:string|symbolaccess:{get():unknown,set(value:unknown):void}static:booleanprivate:boolean})=>(initialValue:unknown)=>unknown|void

属性装饰器的用例与方法装饰器的用法也非常相似。例如,可以跟踪对属性的访问或将其标记为已弃用:

functiondeprecatedProperty(_:any,context){if(context.kind==="field"){returnfunction(initialValue:any){console.log(`${context.name}is deprecated and will be removed in a future version.`)returninitialValue}}}

代码与为方法定义的 deprecatedMethod 装饰器非常相似,它的用法也是如此。

Summer:访问器装饰器

与方法装饰器非常相似的是访问器装饰器,它是针对 getter 和 setter 的装饰器:

type ClassSetterDecorator=(target:Function,context:{kind:"setter"name:string|symbolaccess:{set(value:unknown):void}static:booleanprivate:booleanaddInitializer(initializer:()=>void):void})=>Function|voidtype ClassGetterDecorator=(value:Function,context:{kind:"getter"name:string|symbolaccess:{get():unknown}static:booleanprivate:booleanaddInitializer(initializer:()=>void):void})=>Function|void

访问器装饰器的定义与方法装饰器的定义类似。例如,可以将 deprecatedMethod 和 deprecatedProperty 修饰合并到一个已弃用的函数中,该函数也支持 getter 和 setter:

functiondeprecated(target,context){constkind=context.kindconstmsg=`${context.name}is deprecated and will be removed in a future version.`if(kind==="method"||kind==="getter"||kind==="setter"){returnfunction(...args:any[]){console.log(msg)returntarget.apply(this,args)}}elseif(kind==="field"){returnfunction(initialValue:any){console.log(msg)returninitialValue}}}

三、装饰器的用例 Summer IS HERE

上面介绍了装饰器是什么以及如何正确使用它们,下面来看看装饰器可以帮助我们解决的一些具体问题。

Summer:计算执行时间

假设想要估计运行一个函数需要多长时间,以此来衡量应用的性能。可以创建一个装饰器来计算方法的执行时间并将其打印在控制台上:

classRocket{@measurelaunch(){console.log("3... 2... 1... 🚀");}}

Rocket 类内部有一个 launch方法。要测量launch方法的执行时间,可以附加measure 装饰器:

import{performance}from"perf_hooks";functionmeasure(target:Function,context){if(context.kind==="method"){returnfunction(...args:any[]){conststart=performance.now()constresult=target.apply(this,args)constend=performance.now()console.log(`Time:${end-start}s`)returnresult}}}

可以看到,measure装饰器会替换原始方法,并使用新方法来计算原始方法的执行时间并将其打印到控制台。为了计算执行时间,可以使用 Node.js 标准库中的性能钩子(Performance Hooks)API。实例化一个新的Rocket对象并调用launch方法:

constrocket=newRocket()rocket.launch()

将得到以下结果:

3...2...1...🚀Time:1.062355000525713s

Summer:使用装饰器工厂函数

要将装饰器配置为在特定场景中采取不同的行为,可以使用装饰器工厂。装饰器工厂是返回装饰器的函数。这样就能够通过在工厂中传递一些参数来自定义装饰器的行为。

来看下面的例子:

functionfill(value:number){returnfunction(_,context){if(context.kind==="field"){returnfunction(initialValue:number){returnvalue+initialValue}}}}

fill 函数返回一个装饰器,根据从工厂传入的值来改变属性的值:

classRocket{@fill(20)fuel:number=50}constrocket=newRocket()console.log(rocket.fuel)// 70

Summer:自动错误拦截

装饰器的另一个常见用例是检查方法调用的前置条件和后置条件。例如,假设要在调用 launch() 方法之前确保 Fuel 至少为给定值:

classRocket{fuel=50launch(){console.log("3... 2... 1... 🚀")}}

假设有一个 Rocket 类,它有一个 launchToMars 方法。要发射火箭,燃料(fuel)必须高于一个值,例如 75。

下面来为它创建装饰器:

functionminimumFuel(fuel:number){returnfunction(target:Function,context){if(context.kind==="method"){returnfunction(...args:any[]){if(this.fuel>fuel){returntarget.apply(this,args)}else{console.log(`Not enough fuel. Required:${fuel}, got${this.fuel}`)}}}}}

minimumFuel是一个工厂装饰器。它接受一个 fuel 参数,表示启动特定火箭所需的燃料量。为了检查燃料条件,将原始方法包裹在一个新方法中。注意,在运行时可以自由地引用 this.fuel。

现在就可以将装饰器应用到launch方法上,并设置最低燃料量:

classRocket{fuel=50@minimumFuel(75)launch(){console.log("3... 2... 1... 🚀")}}

如果现在调用 launch 方法,它不会发射火箭,因为当前的燃料量为 50:

constrocket=newRocket()rocket.launch()Not enough fuel.Required:75,got50

[1]装饰器提案: https://github.com/tc39/proposal-decorators。

9    2023-08-07 16:07:42    JavaScript ES6 类