TypeScript 5.0 新特性速览: Decorators

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 进行修改,任何普通函数都可以作为装饰器被调用。

装饰器会接收到两个参数:

  1. 将被装饰的值(类、类方法、类字段、类访问器)
  2. 一个包含被装饰的值的信息的 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;

其中,InputOutput 分别为传递给装饰器的值从该装饰器返回的值,如果装饰器没有返回值,则被装饰的值不会被修改。根据被装饰的值不同,该结构中的部分字段会有差异。

装饰器的作用方式

  1. 通过在装饰器中返回一个和被装饰值同样语义的返回值来对原值进行替换,没有返回值则沿用原值。
  2. 通过 access 对象提供访问器来读取或修改被修饰的值(是否可修改取决于值的种类)
  3. 通过 addInitializer 来添加类中字段的初始化逻辑
  4. 在值被完全定义之后执行额外代码

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
// class 装饰器,如果有 export,装饰器需要在 export 之后
export @classDecorator class DecoratorTest {

// 方法装饰器
@methodDecorator
method() { }

// 字段装饰器
@fieldDecorator x = 1;

// getter 装饰器
@getterDecorator
get y() {
return 2;
}

// setter 装饰器
@setterDecorator
set y(value: number) {
// do nothing
}

// accessor 装饰器
@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

1
2
3
wdnmd
wdnmd
wdnmd

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

1
12 false

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. 装饰器的调用时机

  1. 对装饰器求值:

    装饰器的排序为:从上到下、从左到右。将装饰器作为表达式求值,其结果将被暂存,并在类定义完成后立即调用。

  2. 调用装饰器:

    根据装饰器类型的不同来调用所有装饰器,调用顺序是逆向,如:

    @f @g value 等价于 f(g(value))

  3. 应用装饰器:

    全部装饰器都调用完成后,将应用装饰器,装饰器应用的中间过程无法观察,在应用所有方法和非静态字段的装饰器之前,新创建的类不可用。

    在所有方法和字段的的装饰器调用后,才会调用类装饰器。

    在类装饰器调用完成后,执行并应用静态字段。

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) {
// do nothing
}

@accessorDecorator accessor z = 3;
}

调用顺序:

  1. 先调用 method, getter/setter, accessor 的装饰器,顺序为书写代码的顺序
  2. 再调用 field 的装饰器,顺序为书写代码的顺序
  3. 最后调用 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;
}
  1. kind: string

    被装饰的字段的种类

    取值 "class" | "method" | "getter" | "setter" | "field" | "accessor"

  2. name: string | symbol
    值的名称,在私有元素的情况下是它的描述(如字段名)。

  3. 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

  4. static: boolean
    该值是否为 static

  5. private: boolean
    该值是否为 private

  6. 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) {
// 显式标注 this
return function <This>(
this: This,
// 标注将要被标注的字段的类型
value: (...args: any[]) => void,
// 传递 Context 的 This 和 Value 范型参数
_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");

类型的严格程度应该取决于你的使用场景,考虑到一个装饰器使用的次数远比编写/修改的次数要多,因此强烈建议编写具有更严格类型标注的装饰器。

相关链接


TypeScript 5.0 新特性速览: Decorators
http://example.com/2023/01/30/ts-5.0-decorators/
作者
Freesia
发布于
2023年1月30日
许可协议