TypeScript 装饰器完全指南
本文大多部分参考A Complete Guide to TypeScript Decorators ,对作者表示感谢~
概览 装饰器本质上是一种特殊的函数被应用于:
所以装饰器其实很像是在组合一系列函数,类似于高阶函数和类。通过装饰器我们可以轻松实现代理模式使代码更简洁以及实现一些更有趣的能力。
装饰器的语法十分简单,只需要在想使用的装饰器前面加上@
符号,装饰器就会被应用到目标上:
1 2 3 4 5 6 function decorator ( ) { console .log ('I am a decorator' ); } @decorator class A {}
装饰器的类型一共可以分为:
类装饰器
属性装饰器
方法装饰器
访问器装饰器
参数装饰器
让我们快速认识一下这 5 种装饰器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @classDecorator class Bird { @propertyDecorator name : string ; @functionDecorator getName ( @paramsDecorator nickName: string ) { return `${this .name} (${nickName} )` ; } @accessorDecorator get bridName () { return this .name ; } }
装饰器的写法可分为:普通装饰器(无法传参) 和 装饰器工厂(可以传参)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function normalDecorator (target: any ) { console .log (target); }; @normalDecorator class Person {}function decoratorFactory (params: string ) { return function (target: any ) { console .log (target, params); }; }; @decoratorFactory ('params' )class Bird {}
执行 执行时机 装饰器只在解释执行时被应用一次,例如:
1 2 3 4 5 6 7 8 9 function f (C ) { console .log ('apply decorator' ) return C } @f class A {}
在终端中会打印apply decorator
,尽管我们没有使用A
类。
执行顺序 不同类型的装饰器的执行顺序是有明确定义的:
首先是实例成员, 参数装饰器 => 方法/访问器/属性 装饰器;
其次是静态成员, 参数装饰器 => 方法/访问器/属性 装饰器;
然后是应用在构造函数上的参数装饰器;
最后是应用在类上的类装饰器;
例如,考虑一下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function f (key: string ): any { console .log ("evaluate: " , key); return function ( ) { console .log ("call: " , key); }; } @f ("Class Decorator" )class C { @f ("Static Property" ) static prop?: number ; @f ("Static Method" ) static method (@f ("Static Method Parameter" ) foo: any ) {} constructor (@f ("Constructor Parameter" ) foo: any ) {} @f ("Instance Method" ) method (@f ("Instance Method Parameter" ) foo: any ) {} @f ("Instance Property" ) prop?: number ; }
以上代码将在控制台打印如下信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 evaluate: Instance Method evaluate: Instance Method Parameter call: Instance Method Parameter call: Instance Method evaluate: Instance Property call: Instance Property evaluate: Static Property call: Static Property evaluate: Static Method evaluate: Static Method Parameter call: Static Method Parameter call: Static Method evaluate: Class Decorator evaluate: Constructor Parameter call: Constructor Parameter call: Class Decorator
你也许会注意到实例属性prop
晚于实例方法method
,但是静态属性prop
早于静态方法method
,这是因为对于属性/方法/访问器装饰器而言,执行顺序取决于它们的声明顺序。
然而,同一方法中不同参数的装饰器的执行顺序是相反的, 最后一个参数的装饰器会最先被执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 function f (key: string ): any { console .log ("evaluate: " , key); return function ( ) { console .log ("call: " , key); }; } class C { method ( @f ("Parameter Foo" ) foo, @f ("Parameter Bar" ) bar ) {}}
上面代码在控制台会打印出:
1 2 3 4 evaluate: Parameter Foo evaluate: Parameter Bar call: Parameter Bar call: Parameter Foo
多个装饰器的组合 你可以对同一目标应用多个装饰器,它们的组合顺序为:
求值外层装饰器
求值内层装饰器
调用内层装饰器
调用外层装饰器
例如:
1 2 3 4 5 6 7 8 9 10 11 12 function f (key: string ) { console .log ("evaluate: " , key); return function ( ) { console .log ("call: " , key); }; } class C { @f ("Outer Method" ) @f ("Inner Method" ) method ( ) {} }
以上代码将会在控制台打印出:
1 2 3 4 evaluate: Outer Method evaluate: Inner Method call: Inner Method call: Outer Method
定义 类装饰器 类型声明:
1 type ClassDecorator = <TFunction extends Function >(target: TFunction ) => TFunction | void ;
因此,类装饰器适合用于继承一个现有类并添加一些属性和方法。
例如我们可以添加一个toString
方法给所有的类来覆盖它原有的toString
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type Consturctor = { new (...args : any []): any };function toString<T extends Consturctor >(BaseClass : T) { return class extends BaseClass { toString ( ) { return JSON .stringify (this ); } }; } @toString class C { public foo = "foo" ; public num = 24 ; } console .log (new C ().toString ());
遗憾的是装饰器并没有类型保护,这意味着:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function reportableClassDecorator<T extends { new (...args : any []): any }>(constructor : T) { return class extends constructor { reportingURL = "http://www..." ; }; } @reportableClassDecorator class BugReport { type = "report" ; title : string ; constructor (t: string ) { this .title = t; } } const bug = new BugReport ("Needs dark mode" );console .log (bug.title ); console .log (bug.type ); bug.reportingURL ;
这是一个TypeScript的已知的缺陷 。 目前我们能做的只有额外提供一个类用于提供类型信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function reportableClassDecorator<T extends { new (...args : any []): any }>(constructor : T) { return class extends constructor { reportingURL = "http://www..." ; }; } class Base { reportingURL!: string ; } @reportableClassDecorator class BugReport extends Base { type = "report" ; title : string ; constructor (t: string ) { super (); this .title = t; } } const bug = new BugReport ("Needs dark mode" );console .log (bug.title ); console .log (bug.type ); bug.reportingURL ;
属性装饰器 类型声明:
1 type PropertyDecorator = (target: Object , propertyKey: string | symbol ) => void ;
参数
target
: 对于静态成员来说是类的构造函数,对于实例成员来说是类的原型;
propertyKey
:属性名称
返回:返回值将被忽略。
除了用于收集信息外,属性装饰器也可以用来给类添加额外的方法和属性。 例如我们可以写一个装饰器来给某些属性添加监听器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 function capitalizeFirstLetter (str: string ) { return str.charAt (0 ).toUpperCase () + str.slice (1 ); } function observable (target: any , key: string ): any { const targetKey = "on" + capitalizeFirstLetter (key) + "Change" ; target[targetKey] = function (fn: (prev: any , next: any ) => void ) { let prev = this [key]; Reflect .defineProperty (this , key, { set (next ) { fn (prev, next); prev = next; } }) }; } class C { @observable foo = -1 ; @observable bar = "bar" ; } const c = new C ();c.onFooChange ((prev, next ) => console .log (`prev: ${prev} , next: ${next} ` )) c.onBarChange ((prev, next ) => console .log (`prev: ${prev} , next: ${next} ` )) c.foo = 100 ; c.foo = -3.14 ; c.bar = "baz" ; c.bar = "sing" ;
方法装饰器 类型声明:
1 2 3 type MethodDecorator = <T>( target: Object , propertyKey: string | symbol , descriptor: TypedPropertyDescriptor<T> ) => TypedPropertyDescriptor <T> | void ;
参数
target
: 对于静态成员来说是类的构造函数,对于实例成员来说是类的原型;
propertyKey
:属性名称
descriptor
: 属性描述器
返回:如果返回了值,它会被用于替代属性的描述器。
方法装饰器不同于属性装饰器的地方在于descriptor
参数。 通过这个参数我们可以修改方法原本的实现,添加一些共用逻辑。 例如我们可以给一些方法添加打印输入与输出的能力:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function logger (target: any , propertyKey: string , descriptor: PropertyDescriptor ) { const original = descriptor.value ; descriptor.value = function (...args ) { console .log ('params: ' , ...args); const result = original.call (this , ...args); console .log ('result: ' , result); return result; } } class C { @logger add (x: number , y:number ) { return x + y; } } const c = new C ();c.add (1 , 2 );
访问器装饰器 访问器装饰器总体上讲和方法装饰器很接近,唯一的区别在于描述器中有的key不同:
方法装饰器的描述器的key为:
configurable
enumerable
writable
value
访问器装饰器的key为:
configurable
enumerable
get
set
例如,我们可以将某个属性设为不可变值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function immutable (target: any , propertyKey: string , descriptor: PropertyDescriptor ) { const original = descriptor.set ; descriptor.set = function (value: any ) { return original.call (this , { ...value }) } } class C { private _point = { x : 0 , y : 0 } @immutable set point (value: { x: number , y: number } ) { this ._point = value; } get point () { return this ._point ; } } const c = new C ();const point = { x : 1 , y : 1 }c.point = point; console .log (c.point === point)
参数装饰器 类型声明:
1 type ParameterDecorator = (target: Object , propertyKey: string | symbol , parameterIndex: number ) => void ;
参数
target
: 对于静态成员来说是类的构造函数,对于实例成员来说是类的原型;
propertyKey
:属性名称(注意是方法 的名称,而不是参数的名称)
parameterIndex
: 参数在方法参数列表中所处位置的下标
返回:返回值会被忽略
单独的参数装饰器能做的事情很有限,它一般都被用于记录可被其它装饰器使用的信息。
结合 对于一些复杂场景, 我们可能需要结合使用不同的装饰器。 例如如果我们不仅想给我们的接口添加静态检查,还想加上运行时检查的能力。
我们可以用3个步骤来实现这个功能:
标记需要检查的参数 (因为参数装饰器先于方法装饰器执行)。
改变方法的descriptor
的value
的值,先运行参数检查器,如果失败就抛出异常。
运行原有的接口实现。
以下是代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 type Validator = (x: any ) => boolean ;const validateMap : Record <string , Validator []> = {};function typedDecoratorFactory (validator: Validator ): ParameterDecorator { return (_, key, index ) => { const target = validateMap[key as string ] ?? []; target[index] = validator; validateMap[key as string ] = target; } } function validate (_: Object , key: string , descriptor: PropertyDescriptor ) { const originalFn = descriptor.value ; descriptor.value = function (...args: any [] ) { const validatorList = validateMap[key]; if (validatorList) { args.forEach ((arg, index ) => { const validator = validatorList[index]; if (!validator) return ; const result = validator (arg); if (!result) { throw new Error ( `Failed for parameter: ${arg} of the index: ${index} ` ); } }); } return originalFn.call (this , ...args); } } const isInt = typedDecoratorFactory ((x ) => Number .isInteger (x));const isString = typedDecoratorFactory ((x ) => typeof x === 'string' );class C { @validate sayRepeat (@isString word: string , @isInt x: number ) { return Array (x).fill (word).join ('' ); } } const c = new C ();c.sayRepeat ('hello' , 2 ); c.sayRepeat ('' , 'lol' as any );
正如例子中展示的, 对我们来说同时理解不同种类装饰器的执行顺序和职责都很重要。
元数据 严格地说,元数据和装饰器是EcmaScript中两个独立的部分。 然而,如果你想实现像是反射 )这样的能力,你总是同时需要它们。
如果我们回顾上一个例子,如果我们不想写各种不同的检查器呢? 或者说,能否只写一个检查器能够通过我们编写的TS类型声明来自动运行类型检查?
有了reflect-metadata 的帮助, 我们可以获取编译期的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import 'reflect-metadata' ;function validate ( target: any , key: string , descriptor: PropertyDescriptor ) { const originalFn = descriptor.value ; const designParamTypes = Reflect .getMetadata ('design:paramtypes' , target, key); descriptor.value = function (...args: any [] ) { args.forEach ((arg, index ) => { const paramType = designParamTypes[index]; const result = arg.constructor === paramType || arg instanceof paramType; if (!result) { throw new Error ( `Failed for validating parameter: ${arg} of the index: ${index} ` ); } }); return originalFn.call (this , ...args); } } @Reflect .metadata ('type' , 'class' )class C { @validate sayRepeat (word: string , x: number ) { return Array (x).fill (word).join ('' ); } } const c = new C ();c.sayRepeat ('hello' , 2 ); c.sayRepeat ('' , 'lol' as any );
以上代码Playground
目前为止一共有三种编译期类型可以拿到:
design:type
: 属性的类型。
desin:paramtypes
: 方法的参数的类型。
design:returntype
: 方法的返回值的类型。
这三种方式拿到的结果都是构造函数(例如String
和Number
)。规则是:
number -> Number
string -> String
boolean -> Boolean
void/null/never -> undefined
Array/Tuple -> Array
Class -> 类的构造函数
Enum -> 如果是纯数字枚举则为Number
, 否则是 Object
Function -> Function
其余都是Object
何时使用 现在我们可以对于何时使用装饰器得出结论, 在阅读上面的代码中你可能也有所感觉。
我将例举一些常用的使用场景:
Before/After钩子
监听属性改变或者方法调用
对方法的参数做转换
添加额外的方法和属性
运行时类型检查
自动编解码
依赖注入
参考文章