2023 年 1 月 26 日,微软发布了 TypeScript 5.0 Beta 版本,其中最为重磅的新特性是 Decorators
为什么「装饰器」是一个新特性? 在之前的 TypeScript 版本中,装饰器已经可以通过 tsc --experimentalDecorators
来使用,但通过这个方式使用的装饰器与 TC39 Decorators Proposal 的规范并不相同,“新”装饰器提案于 2022 年 4 月底进入 Stage 3,这意味着该提案已基本完成,因此,在 TypeScript 5.0 中对该提案的实现是一个“新特性”。本文后续内容都基于新的装饰器实现。
如何体验新特性
新装饰器语法速览 1. 装饰器是什么 装饰器是一个函数,没有特殊语法,通过 @ 的方式调用,能够对被装饰的 classes, methods, fields, accessors, getters/setters
进行修改,任何普通函数都可以作为装饰器被调用。
装饰器会接收到两个参数:
将被装饰的值(类、类方法、类字段、类访问器)
一个包含被装饰的值的信息的 context 对象
装饰器的一般结构 1 2 3 4 5 6 7 8 9 10 11 type Decorator = (value: Input, context: { kind: string ; name: string | symbol ; access: { get?(): unknown ; set?(value: unknown ): void ; }; private ?: boolean ; static ?: boolean ; addInitializer?(initializer: () => void ): void ; } ) => Output | void ;
其中,Input
和 Output
分别为传递给装饰器的值 和从该装饰器返回的值 ,如果装饰器没有返回值,则被装饰的值不会被修改。根据被装饰的值不同,该结构中的部分字段会有差异。
装饰器的作用方式
通过在装饰器中返回一个和被装饰值同样语义的 返回值来对原值进行替换,没有返回值则沿用原值。
通过 access
对象提供访问器来读取或修改被修饰的值(是否可修改取决于值的种类)
通过 addInitializer
来添加类中字段的初始化逻辑
在值被完全定义之后执行额外代码
2. 装饰器的使用 使用 @decorator
来调用装饰器
例:
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 export @classDecorator class DecoratorTest { @methodDecorator method ( ) { } @fieldDecorator x = 1 ; @getterDecorator get y () { return 2 ; } @setterDecorator set y (value: number ) { } @accessorDecorator accessor z = 3 ; }
3. 编写不同种类的装饰器简单示例 method 装饰器 类型定义 1 2 3 4 5 6 7 8 type ClassMethodDecorator = (value: Function , context: { kind: "method" ; name: string | symbol ; access: { get(): unknown }; // 暂不可用 static : boolean ; private : boolean ; addInitializer(initializer: () => void ): void ; } ) => Function | void ;
Example 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function repeatFunction (times: number ) { return function (value: any , context: ClassMethodDecoratorContext ) { return function (...args: any [] ) { for (let i = 0 ; i < times; i++) { value.call (this , ...args); } } } }class Methods { @repeatFunction (3 ) logText (text: string ) { console .log (text) } }const m = new Methods (); m.logText ("wdnmd" );
Output
getter/setter 装饰器 类型定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type ClassGetterDecorator = (value: Function , context: { kind: "getter" ; name: string | symbol ; access: { get(): unknown }; static : boolean ; private : boolean ; addInitializer(initializer: () => void ): void ; } ) => Function | void ;type ClassSetterDecorator = (value: Function , context: { kind: "setter" ; name: string | symbol ; access: { set(value: unknown ): void }; static : boolean ; private : boolean ; addInitializer(initializer: () => void ): void ; } ) => Function | void ;
Example 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function getCounter (value: any , context: ClassGetterDecoratorContext ) { let calledTimes = 0 ; const { name } = context; if (typeof name !== 'string' ) { throw new Error ('cannot decorate on a symbol key' ); } return function ( ) { console .log (`${name} getter was called, count` , ++calledTimes); return value.call (this ); } }class Getters { @getCounter get a () { return 1 ; } }const g = new Getters (); g.a ; g.a ; g.a ;
Output 1 2 3 a getter was called, count 1 a getter was called, count 2 a getter was called, count 3
accessor 装饰器 类型定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type ClassAutoAccessorDecorator = ( value: { get: () => unknown ; set(value: unknown ) => void ; }, context: { kind: "accessor" ; name: string | symbol ; access: { get(): unknown , set(value: unknown ): void }; static : boolean ; private : boolean ; addInitializer(initializer: () => void ): void ; } ) => { get?: () => unknown ; set?: (value: unknown ) => void ; init?: (initialValue: unknown ) => unknown ; } | void ;
Example 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 function logged (value: any , { kind, name }: ClassAccessorDecoratorContext ) { if (typeof name !== 'string' ) { return ; } if (kind === "accessor" ) { let { get, set } = value; return { get ( ) { console .log (`getting ${name} ` ); return get.call (this ); }, set (val: any ) { console .log (`setting ${name} to ${val} ` ); return set.call (this , val); }, init (initialValue: any ) { console .log (`initializing ${name} with value ${initialValue} ` ); return initialValue; } }; } }class C { @logged accessor x = 1 ; }let c = new C (); c.x ; c.x = 123 ;
Output 1 2 3 initializing x with value 1 getting x setting x to 123
field 装饰器 类型定义 1 2 3 4 5 6 7 type ClassFieldDecorator = (value: undefined , context: { kind: "field" ; name: string | symbol ; access: { get(): unknown , set(value: unknown ): void }; static : boolean ; private : boolean ; } ) => (initialValue: unknown ) => unknown | void ;
Example 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 function autoPendingNetBarPermission (value: any , context: ClassFieldDecoratorContext ) { return function (_initialVal: boolean ) { if (this .age < 18 ) { return false ; } return true ; } }class Person { constructor (public age: number ) { } }class Student extends Person { constructor (age: number ) { super (age); } @autoPendingNetBarPermission canEnterNetBar : boolean ; }const s = new Student (12 );console .log (s.age , s.canEnterNetBar );
Output
class 装饰器 类型定义 1 2 3 4 5 type ClassDecorator = (value: Function , context: { kind: "class" ; name: string | undefined ; addInitializer(initializer: () => void ): void ; } ) => Function | void ;
Example 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function logged (value: any , { kind, name }: ClassDecoratorContext ) { if (kind === "class" ) { return class extends value { constructor (...args: any [] ) { super (...args); console .log (`constructing an instance of ${name} with arguments ${args.join(", " )} ` ); } } } }@logged class C { constructor (...args: any [] ) { } }new C (1 , 2 , 3 );
Output 1 constructing an instance of C with arguments 1 , 2 , 3
4. 装饰器的调用时机
对装饰器求值:
装饰器的排序为:从上到下、从左到右。将装饰器作为表达式求值,其结果将被暂存,并在类定义完成后立即调用。
调用装饰器:
根据装饰器类型的不同来调用所有装饰器,调用顺序是逆向 ,如:
@f @g value
等价于 f(g(value))
应用装饰器:
全部装饰器都调用完成后,将应用装饰器,装饰器应用的中间过程无法观察,在应用所有方法和非静态字段的装饰器之前,新创建的类不可用。
在所有方法和字段的的装饰器调用后,才会调用类装饰器。
在类装饰器调用完成后,执行并应用静态字段。
example code
:
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 function classDecorator (value: any , context: any ) { console .log ('classDecorator called' ); }function methodDecoratorGenerator (num: number ) { return function (value: any , context: any ) { console .log ('methodDecorator called, num: ' , num); }; }function fieldDecorator (value: any , context: any ) { console .log ('fieldDecorator called' ); }function accessorDecorator (value: any , context: any ) { console .log ('accessorDecorator called' ); }function getterDecorator (value: any , context: any ) { console .log ('getterDecorator called' ); }function setterDecorator (value: any , context: any ) { console .log ('setterDecorator called' ); }export @classDecorator class DecoratorTest { @methodDecoratorGenerator (2 ) @methodDecoratorGenerator (1 ) method ( ) { } @fieldDecorator x = 1 ; @getterDecorator get y () { return 2 ; } @setterDecorator set y (value: number ) { } @accessorDecorator accessor z = 3 ; }
调用顺序:
先调用 method, getter/setter, accessor 的装饰器,顺序为书写代码的顺序
再调用 field 的装饰器,顺序为书写代码的顺序
最后调用 class 的装饰器
输出结果:
1 2 3 4 5 6 7 methodDecorator called , num: 1 methodDecorator called , num: 2 getterDecorator called setterDecorator called accessorDecorator called fieldDecorator called classDecorator called
5. 装饰器 Context 释义 再看一遍 context 的类型结构
1 2 3 4 5 6 7 8 9 10 11 type Context = { kind : string ; name : string | symbol ; access : { get?(): unknown ; set?(value : unknown ): void ; }; private ?: boolean ; static ?: boolean ; addInitializer?(initializer : () => void ): void ; }
kind: string
被装饰的字段的种类
取值 "class" | "method" | "getter" | "setter" | "field" | "accessor"
name: string | symbol
值的名称,在私有元素的情况下是它的描述(如字段名)。
access
Gets/Sets the value of the field on the provided receiver. 详见 https://github.com/tc39/proposal-decorators#access-and-metadata-sidechanneling TypeScript 暂不启用该特性,原因:https://github.com/tc39/proposal-decorators/issues/494
static: boolean
该值是否为 static
private: boolean
该值是否为 private
addInitializer: (initializer: () => void): void
用于添加除 field
之外的任意类型的类成员的初始化逻辑,可以运行任意代码。 代码执行时机: class 装饰器: 在 class 定义完成并完成静态字段初始化完成之后 class element 装饰器(除 field
): 在 class 创建中,在字段初始化之前 class static element: 在 class 定义过程中,在 static 字段定义之前 官方示例:customElement
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function customElement (name ) { return (value, { addInitializer } ) => { addInitializer (function ( ) { customElements.define (name, this ); }); } }@customElement ('my-element' )class MyElement extends HTMLElement { static get observedAttributes () { return ['some' , 'attrs' ]; } }
6. 编写类型安全的装饰器 在上面的例子中,为了减轻阅读负担,并未使用完整的类型标注,如果需要更严格的类型标注,下面将以 Method Decorator
为例
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 repeatFunction (times: number ) { return function <This >( this : This , value : (...args: any [] ) => void , _context : ClassMethodDecoratorContext <This , (...args: any [] ) => void >) { return function (this : This, ...args: any [] ) { for (let i = 0 ; i < times; i++) { value.call (this , ...args); } } } }class Methods { @repeatFunction (3 ) logText (text: string ) { console .log (text) } }const m = new Methods (); m.logText ("wdnmd" );
类型的严格程度应该取决于你的使用场景,考虑到一个装饰器使用的次数远比编写/修改的次数要多,因此强烈建议编写具有更严格类型标注的装饰器。
相关链接