Skip to main content

Decorators (Experimental)

装饰器提供了一种在类声明和成员中添加注释和元编程语法的方式。TypeScript在2014年就支持装饰器,不过使用的是旧语法,2022年装饰器正式通过标准成为新的语法,装饰器的旧语法与标准语法相差较大,本篇将基于旧语法进行介绍。

experimentalDecorators

要启用对装饰器的实验性支持,需要在命令行或者tsconfig.json文件中设置experimentalDecorators编译选项:

// tsc --target ES5 --experimentalDecorators

{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}

装饰器

装饰器是一种特殊类型的声明,可以附加在类声明、方法、访问器、属性或参数上。装饰器使用@expression的形式,其中expression必须为一个在运行时可调用的函数,并提供有关被装饰声明的信息。换句话说,expression应该是一个函数,它会在运行时对装饰的声明进行处理。这个函数可以访问装饰的声明,并根据需要对其进行修改或添加其他行为。

function sealed(target) {
// do something here
}

@sealed

如果想自定义装饰器如何应用于声明,可以编写一个装饰器工厂。装饰器工厂函数本质上就是一个返回在运行时由装饰器调用的表达式的函数。

function decoratorWrapper(value: string) {
return function (target) {
// do something here
}
}

@decoratorWrapper(params)

装饰器合成

多个装饰器可被应用到一个声明中,可在单行或多行:

@f @g x

@f
@g
x

当多个装饰器被应用到单个声明时,它们的结果跟这种数学算法跟相似,如当组合函数f和g时,其组合(*f* ∘ *g*)(*x*)和*f*(*g*(*x*))一样。

在对单个声明评估多个装饰器时,会执行以下步骤:

  1. 每个装饰器的表达式都是从上到下计算的。
  2. 然后将结果从下到上作为函数执行。
function first() {
console.log('first(): factory evaluated');
return function (
target: unknown,
propertyKey: unknown,
) {
console.log("first(): called");
}
}

function second() {
console.log('second(): factory evaluated');
return function (
target: unknown,
propertyKey: unknown,
) {
console.log("second(): called");
}
}

class ExampleClass {
@first()
@second()
method() {
}
}

/**
first(): factory evaluated
second(): factory evaluated
second(): called
first(): called
*/

装饰器中种类

  • 类装饰器(Class Decorators):用于类。
  • 属性装饰器(Property Decorators):用于属性。
  • 方法装饰器(Method Decorators):用于方法。
  • 存取器装饰器(Accessor Decorators):用于类的 setget 方法。
  • 参数装饰器(Parameter Decorators):用于方法的参数。

类装饰器

类装饰器在类声明之前声明。类装饰器应用于类的构造函数,可用于观察、修改或替换类定义。类装饰器不能在声明文件或任何其他环境上下文中使用。其定义如下:

type ClassDecorator = <TFunction extends Function>
(target: TFunction) => TFunction | void;
  • 类装饰器的表达式将在运行时作为函数调用,并将装饰类的构造函数作为其唯一的参数。
  • 默认情况下,类装饰器只会应用于被直接装饰的类,而不会自动传播到子类。
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype)
}

@sealed
class BugReport {
type = "report";
title: string;

constructor(t: string) {
this.title = t;
}
}
  • 如果类装饰器返回一个值,它将用提供的构造函数替换类声明。
  • 装饰器不会改变TypeScript类型。
  • 装饰器会在代码加载阶段执行,而不是在运行时执行,而且只会执行一次。
  • 由于TypeScript存在编译阶段,所以装饰器对类的行为的改变,实际上发生在编译阶段。这意味着,TypeScript 装饰器能在编译阶段运行代码,也就是说,它本质就是编译时执行的函数。
function reportableClassDecorator<T extends { new (...args: any[]): {}}>(constructor: T) {
return class extends constructor {
reportingURL = 'https://www...'
}
}

// 由于返回了一个值 会使用新的类替换旧的类 即继承后的结果
@reportableClassDecorator
class BugReport {
type = "report";
title: string;

constructor(t: string) {
this.title = t;
}
}

const bug = new BugReport("Needs dark mode");
/**
BugReport {
type: 'report',
title: 'Needs dark mode',
reportingURL: 'https://www...'
}
*/
console.log(bug);

// 装饰器不会改变TypeScritpt类型
// Property 'reportingURL' does not exist on type 'BugReport'.
bug.reportingURL

方法装饰器

方法装饰器在方法声明之前声明,用于方法的属性描述符,可以用于观察、修改或替代方法定义。方法装饰器不能在声明文件、重载或其他环境上下文中使用。其定义如下:

type MethodDecorator = <T>(
target: Object,
propertyKey: string|symbol,
descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;

ƒ 方法装饰器可接受三个参数:

  • target:(对于类的静态方法)类的构造函数,或者(对于类的实例方法)类的原型。
  • propertyKey:所装饰方法的方法名,类型为string | symbol
  • descriptor:所装饰方法的描述对象。

如果方法装饰器的返回了一个值,就是修改后的该方法的描述对象,可以覆盖原始方法的描述对象。

function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value
console.log(target, propertyKey, descriptor);

}
}

class Greeter {
greeting: string;

constructor(message: string) {
this.greeting = message
}

@enumerable(true)
greet() {
return `Hello, ${this.greeting}`
}
}

属性装饰器

属性装饰器在属性声明之前声明,它不能在声明文件或其他环境上下文中使用。其定义如下:

type PropertyDecorator =
(
target: Object,
propertyKey: string | symbol
) => void;

属性装饰器函数接受两个参数:

  • target:(对于实例属性)类的原型对象(prototype),或者(对于静态属性)类的构造函数。
  • propertyKey:所装饰属性的属性名,注意类型有可能是字符串,也有可能是Symbol值。

属性装饰器不需要返回值,如果有的话,也会被忽略。

class Greeter {
@format('YYYY-MM-DD')
date: string;

constructor(date: string) {
this.date = date;
}

greet() {
}
}

function format(formatString: string) {
return function (target: any, propertyKey: string) {
// ...
}
}

参数装饰器

参数装饰器在参数声明之前声明,应用于类构造函数或方法声明的函数。参数装饰器不能在声明文件、重载或其他环境上下文中使用。参数装饰器主要用于输出信息,没法改变类的行为。其类型定义如下:

type ParameterDecorator = (
target: Object,
propertyKey: string|symbol,
parameterIndex: number
) => void;

参数装饰器接受三个参数:

  • target:(对于静态方法)类的构造函数,或者(对于类的实例方法)类的原型对象。
  • propertyKey:所装饰的方法的名字,类型为string | symbol
  • parameterIndex:当前参数在方法的参数序列的位置(从0开始)。

参数装饰器不需要返回值,如果有的话,也会被忽略。

function log(target: any, propertyKey: string | symbol, parameterIndex: number) {
console.log(target, propertyKey, parameterIndex);
}

class C {
member(
@log x: number,
@log y: number
) {
console.log('x, y ->', x, ',', y);

}
}

const c = new C();
c.member(1, 2);

存取器装饰器

访问器装饰器在访问器声明之前声明,它应用于访问器的属性描述符,可用于观察、修改或替代访问器的定义。访问器装饰器不能在声明文件或其他环境上下文中使用。存取器装饰器的类型定义,与方法装饰器一致:

type AccessorDecorator = <T>(
target: Object,
propertyKey: string|symbol,
descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;

存取器装饰器有三个参数。

  • target:(对于静态属性的存取器)类的构造函数,或者(对于实例属性的存取器)类的原型。
  • propertyKey:存取器的属性名。
  • descriptor:存取器的属性描述对象。

存取器装饰器如果有返回值,那么返回值会作为该属性新的描述对象。

function configurable(value: boolean) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.enumerable = value
}
}

class Point {
private _x: number;
private _y: number;

constructor(x: number, y: number) {
this._x = x;
this._y = y;
}

@configurable(false)
get x() {
return this._x
}

@configurable(false)
get y() {
return this._y
}
}

装饰器执行顺序

装饰器只会代码解析时执行一次,并按照如下顺序执行:

  1. 实例相关的装饰器。
  2. 静态相关的装饰器。
  3. 构造方法的参数装饰器。
  4. 类装饰器。
function f(key:string): any {
return function () {
console.log('Running', key);
};
}

@f('class')
class C {

@f('instance property')
value : string;

@f('static method')
static method() {}

@f('instance method')
method() {}

constructor(@f('constructor parameter') foo:any) {}
}

/*
Running: instance property
Running: instance method
Running: static method
Running: constructor parameter
Running: class
*/

同一级装饰器的执行顺序,是按照它们的代码顺序。但是,参数装饰器的执行总是早于方法装饰器。

function f(key:string): any {
return function (target: any, propertyKey: string) {
console.log('Running:', key);
};
}

class C {

@f('Method 2')
m2(@f('Parameter 2') foo:any) {}

@f('Property 2')
p2: number;

@f('Method 1')
m1(@f('Parameter 1') foo:any) {}

@f('Property 1')
p1: number;
}

/*
Running: Parameter 2
Running: Method 2
Running: Property 2
Running: Parameter 1
Running: Method 1
Running: Property 1
*/

如果同一个方法或属性有多个装饰器以及一个方法有多个参数装饰器,那么装饰器将顺序加载、逆序执行。

function f(key:string): any {
return function (target: any, propertyKey: string) {
console.log('Running:', key);
};
}

class C {
@f('A')
@f('B')
@f('C')
m1(
@f('D') x: number,
@f('E') y: number
) {}
}

/*
Running: E
Running: D
Running: C
Running: B
Running: A
*/