装饰器

注意 本文档涉及实验性的阶段 2 装饰器实现。自 Typescript 5.0 起,已支持阶段 3 装饰器。 参考:Typescript 5.0 中的装饰器

介绍

🌐 Introduction

随着 TypeScript 和 ES6 中类的引入,现在存在某些场景需要额外的功能来支持对类和类成员进行注解或修改。 装饰器提供了一种方法,可以为类声明和成员添加注解以及元编程语法。

🌐 With the introduction of Classes in TypeScript and ES6, there now exist certain scenarios that require additional features to support annotating or modifying classes and class members. Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members.

延伸阅读(第2阶段):TypeScript 装饰器完整指南

要启用对装饰器的实验性支持,你必须在命令行或在你的 tsconfig.json 中启用 experimentalDecorators 编译器选项:

🌐 To enable experimental support for decorators, you must enable the experimentalDecorators compiler option either on the command line or in your tsconfig.json:

命令行

shell
tsc --target ES5 --experimentalDecorators

tsconfig.json:

{
"": "ES5",
}
}

装饰器

🌐 Decorators

一个 装饰器 是一种特殊的声明,可以附加到class 声明方法访问器属性参数上。装饰器使用 @expression 的形式,其中 expression 必须求值为一个函数,该函数将在运行时使用有关被装饰声明的信息被调用。

🌐 A Decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use the form @expression, where expression must evaluate to a function that will be called at runtime with information about the decorated declaration.

例如,给定装饰器 @sealed,我们可以如下编写 sealed 函数:

🌐 For example, given the decorator @sealed we might write the sealed function as follows:

ts
function sealed(target) {
// do something with 'target' ...
}

装饰器工厂

🌐 Decorator Factories

如果我们想自定义装饰器应用于声明的方式,我们可以编写一个装饰器工厂。

装饰器工厂(Decorator Factory)只是一个函数,它返回一个表达式,该表达式将在运行时由装饰器调用。

🌐 If we want to customize how a decorator is applied to a declaration, we can write a decorator factory. A Decorator Factory is simply a function that returns the expression that will be called by the decorator at runtime.

我们可以用以下方式编写一个装饰器工厂:

🌐 We can write a decorator factory in the following fashion:

ts
function color(value: string) {
// this is the decorator factory, it sets up
// the returned decorator function
return function (target) {
// this is the decorator
// do something with 'target' and 'value'...
};
}

装饰器组成

🌐 Decorator Composition

可以将多个装饰器应用于声明,例如在一行中:

🌐 Multiple decorators can be applied to a declaration, for example on a single line:

ts
@f @g x
Try

在多行上:

🌐 On multiple lines:

ts
@f
@g
x
Try

当多个装饰器应用于单个声明时,它们的评估类似于数学中的函数组合。在这种模型中,当组合函数 fg 时,得到的复合函数 (fg)(x) 等价于 f(g(x))。

🌐 When multiple decorators apply to a single declaration, their evaluation is similar to function composition in mathematics. In this model, when composing functions f and g, the resulting composite (fg)(x) is equivalent to f(g(x)).

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

🌐 As such, the following steps are performed when evaluating multiple decorators on a single declaration in TypeScript:

  1. 每个装饰器的表达式都是从上到下计算的。
  2. 然后将结果作为函数从下到上调用。

如果我们使用 装饰器工厂,我们可以通过以下例子观察这个评估顺序:

🌐 If we were to use decorator factories, we can observe this evaluation order with the following example:

ts
function first() {
console.log("first(): factory evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("first(): called");
};
}
 
function second() {
console.log("second(): factory evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("second(): called");
};
}
 
class ExampleClass {
@first()
@second()
method() {}
}
Try

这会将这个输出打印到控制台:

🌐 Which would print this output to the console:

shell
first(): factory evaluated
second(): factory evaluated
second(): called
first(): called

装饰器评价

🌐 Decorator Evaluation

应用于类内部各种声明的装饰器如何应用有一个明确定义的顺序:

🌐 There is a well defined order to how decorators applied to various declarations inside of a class are applied:

  1. 参数装饰器,随后是_方法_、存取器_或_属性装饰器,会应用于每个实例成员。
  2. 参数装饰器,随后是_方法_、访问器_或_属性装饰器,会应用于每个静态成员。
  3. 参数装饰器 应用于构造函数。
  4. 类装饰器 应用于该类。

类装饰器

🌐 Class Decorators

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

🌐 A Class Decorator is declared just before a class declaration. The class decorator is applied to the constructor of the class and can be used to observe, modify, or replace a class definition. A class decorator cannot be used in a declaration file, or in any other ambient context (such as on a declare class).

类装饰器的表达式将在运行时作为函数调用,装饰类的构造函数作为其唯一参数。

🌐 The expression for the class decorator will be called as a function at runtime, with the constructor of the decorated class as its only argument.

如果类装饰器返回一个值,它将用提供的构造函数替换类声明。

🌐 If the class decorator returns a value, it will replace the class declaration with the provided constructor function.

注意 如果你选择返回一个新的构造函数,你必须注意保持原始的原型。 在运行时应用装饰器的逻辑不会为你自动补齐这一步。

以下是将类装饰器(@sealed)应用于 BugReport 类的示例:

🌐 The following is an example of a class decorator (@sealed) applied to a BugReport class:

ts
@sealed
class BugReport {
type = "report";
title: string;
 
constructor(t: string) {
this.title = t;
}
}
Try

我们可以使用以下函数声明来定义 @sealed 装饰器:

🌐 We can define the @sealed decorator using the following function declaration:

ts
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}

当执行 @sealed 时,它会封闭构造函数及其原型,因此在运行时通过访问 BugReport.prototype 或在 BugReport 本身上定义属性将无法向该类添加或移除任何功能(注意 ES2015 类实际上只是基于原型的构造函数的语法糖)。该装饰器不会阻止类继承 BugReport

🌐 When @sealed is executed, it will seal both the constructor and its prototype, and will therefore prevent any further functionality from being added to or removed from this class during runtime by accessing BugReport.prototype or by defining properties on BugReport itself (note that ES2015 classes are really just syntactic sugar to prototype-based constructor functions). This decorator does not prevent classes from sub-classing BugReport.

接下来我们有一个示例,说明如何覆盖构造函数以设置新的默认值。

🌐 Next we have an example of how to override the constructor to set new defaults.

ts
function reportableClassDecorator<T extends { new (...args: 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); // Prints "Needs dark mode"
console.log(bug.type); // Prints "report"
 
// Note that the decorator _does not_ change the TypeScript type
// and so the new property `reportingURL` is not known
// to the type system:
bug.reportingURL;
Property 'reportingURL' does not exist on type 'BugReport'.2339Property 'reportingURL' does not exist on type 'BugReport'.
Try

方法装饰器

🌐 Method Decorators

方法装饰器在方法声明之前声明。装饰器应用于该方法的属性描述符(Property Descriptor),可以用来观察、修改或替换方法定义。方法装饰器不能在声明文件、重载上或任何其他外部上下文中使用(例如在一个 declare 类中)。

🌐 A Method Decorator is declared just before a method declaration. The decorator is applied to the Property Descriptor for the method, and can be used to observe, modify, or replace a method definition. A method decorator cannot be used in a declaration file, on an overload, or in any other ambient context (such as in a declare class).

方法装饰器的表达式将在运行时作为函数调用,并带有以下三个参数:

🌐 The expression for the method decorator will be called as a function at runtime, with the following three arguments:

  1. 静态成员的类的构造函数,或者实例成员的类的原型。
  2. 成员的名称。
  3. 该成员的 属性描述符

注意  如果你的脚本目标版本低于 ES5,则 属性描述符 将是 undefined

如果方法装饰器返回一个值,它将被用作该方法的 属性描述符

🌐 If the method decorator returns a value, it will be used as the Property Descriptor for the method.

注意  如果你的脚本目标版本低于 ES5,则返回值将被忽略。

下面是一个方法装饰器(@enumerable)应用于 Greeter 类方法的示例:

🌐 The following is an example of a method decorator (@enumerable) applied to a method on the Greeter class:

ts
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
 
@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}
Try

我们可以使用以下函数声明来定义 @enumerable 装饰器:

🌐 We can define the @enumerable decorator using the following function declaration:

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

@enumerable(false) 装饰器在这里是一个 装饰器工厂。当调用 @enumerable(false) 装饰器时,它会修改属性描述符的 enumerable 属性。

🌐 The @enumerable(false) decorator here is a decorator factory. When the @enumerable(false) decorator is called, it modifies the enumerable property of the property descriptor.

访问器装饰器

🌐 Accessor Decorators

_访问器装饰器_是在访问器声明之前声明的。
访问器装饰器应用于访问器的 属性描述符,可以用于观察、修改或替换访问器的定义。
访问器装饰器不能在声明文件中使用,也不能在任何其他环境上下文中使用(例如在 declare 类中)。

🌐 An Accessor Decorator is declared just before an accessor declaration. The accessor decorator is applied to the Property Descriptor for the accessor and can be used to observe, modify, or replace an accessor’s definitions. An accessor decorator cannot be used in a declaration file, or in any other ambient context (such as in a declare class).

注意:TypeScript 不允许同时装饰单个成员的 getset 访问器。 相反,该成员的所有装饰器必须应用于文档顺序中指定的第一个访问器。 这是因为装饰器应用于 属性描述符,它将 getset 访问器组合在一起,而不是单独应用于每个声明。

访问器装饰器的表达式将在运行时作为函数调用,并带有以下三个参数:

🌐 The expression for the accessor decorator will be called as a function at runtime, with the following three arguments:

  1. 静态成员的类的构造函数,或者实例成员的类的原型。
  2. 成员的名称。
  3. 该成员的 属性描述符

注意  如果你的脚本目标版本低于 ES5,则 属性描述符 将是 undefined

如果访问器装饰器返回一个值,它将被用作该成员的 属性描述符

🌐 If the accessor decorator returns a value, it will be used as the Property Descriptor for the member.

注意  如果你的脚本目标版本低于 ES5,则返回值将被忽略。

下面是一个访问器装饰器(@configurable)应用于 Point 类成员的示例:

🌐 The following is an example of an accessor decorator (@configurable) applied to a member of the Point class:

ts
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;
}
}
Try

我们可以使用以下函数声明来定义 @configurable 装饰器:

🌐 We can define the @configurable decorator using the following function declaration:

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

属性装饰器

🌐 Property Decorators

属性装饰器是在属性声明之前声明的。属性装饰器不能在声明文件中使用,也不能在任何其他环境上下文中使用(例如在 declare 类中)。

🌐 A Property Decorator is declared just before a property declaration. A property decorator cannot be used in a declaration file, or in any other ambient context (such as in a declare class).

属性装饰器的表达式将在运行时作为函数调用,并带有以下两个参数:

🌐 The expression for the property decorator will be called as a function at runtime, with the following two arguments:

  1. 静态成员的类的构造函数,或者实例成员的类的原型。
  2. 成员的名称。

注意 由于 TypeScript 中属性装饰器的初始化方式,_属性描述符_不会作为参数传递给属性装饰器。 这是因为在定义原型的成员时,目前没有机制可以描述实例属性,也无法观察或修改属性的初始化器。返回值也会被忽略。 因此,属性装饰器只能用于观察某个类是否声明了特定名称的属性。

我们可以使用此信息来记录有关属性的元数据,如下例所示:

🌐 We can use this information to record metadata about the property, as in the following example:

ts
class Greeter {
@format("Hello, %s")
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
let formatString = getFormat(this, "greeting");
return formatString.replace("%s", this.greeting);
}
}

然后,我们可以使用以下函数声明来定义 @format 装饰器和 getFormat 函数:

🌐 We can then define the @format decorator and getFormat functions using the following function declarations:

ts
import "reflect-metadata";
const formatMetadataKey = Symbol("format");
function format(formatString: string) {
return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target: any, propertyKey: string) {
return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

@format("Hello, %s") 装饰器在这里是一个 装饰器工厂。当调用 @format("Hello, %s") 时,它使用 reflect-metadata 库中的 Reflect.metadata 函数为属性添加一个元数据条目。当调用 getFormat 时,它会读取格式的元数据值。

🌐 The @format("Hello, %s") decorator here is a decorator factory. When @format("Hello, %s") is called, it adds a metadata entry for the property using the Reflect.metadata function from the reflect-metadata library. When getFormat is called, it reads the metadata value for the format.

注意  这个例子需要 reflect-metadata 库。 有关 reflect-metadata 库的更多信息,请参见 元数据

参数装饰器

🌐 Parameter Decorators

一个 参数装饰器 声明在参数声明之前。参数装饰器应用于类的构造函数或方法的函数。参数装饰器不能在声明文件、重载或任何其他环境上下文中使用(例如在 declare 类中)。

🌐 A Parameter Decorator is declared just before a parameter declaration. The parameter decorator is applied to the function for a class constructor or method declaration. A parameter decorator cannot be used in a declaration file, an overload, or in any other ambient context (such as in a declare class).

参数装饰器的表达式将在运行时作为函数调用,并带有以下三个参数:

🌐 The expression for the parameter decorator will be called as a function at runtime, with the following three arguments:

  1. 静态成员的类的构造函数,或者实例成员的类的原型。
  2. 成员的名称。
  3. 函数参数列表中参数的序号索引。

注意  参数装饰器只能用于观察方法上是否声明了某个参数。

参数装饰器的返回值被忽略。

🌐 The return value of the parameter decorator is ignored.

下面是一个参数装饰器(@required)应用于 BugReport 类成员参数的示例:

🌐 The following is an example of a parameter decorator (@required) applied to parameter of a member of the BugReport class:

ts
class BugReport {
type = "report";
title: string;
 
constructor(t: string) {
this.title = t;
}
 
@validate
print(@required verbose: boolean) {
if (verbose) {
return `type: ${this.type}\ntitle: ${this.title}`;
} else {
return this.title;
}
}
}
Try

然后我们可以使用以下函数声明来定义 @required@validate 装饰器:

🌐 We can then define the @required and @validate decorators using the following function declarations:

ts
import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
 
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata( requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
 
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
let method = descriptor.value!;
 
descriptor.value = function () {
let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
throw new Error("Missing required argument.");
}
}
}
return method.apply(this, arguments);
};
}
Try

@required 装饰器添加一个元数据条目,将参数标记为必需。 然后,@validate 装饰器将现有的 print 方法封装在一个函数中,该函数在调用原方法之前验证参数。

🌐 The @required decorator adds a metadata entry that marks the parameter as required. The @validate decorator then wraps the existing print method in a function that validates the arguments before invoking the original method.

注意  这个例子需要 reflect-metadata 库。 有关 reflect-metadata 库的更多信息,请参见 元数据

元数据

🌐 Metadata

有些示例使用 reflect-metadata 库,它为一个实验性的元数据 API 添加了 polyfill。该库尚未成为 ECMAScript(JavaScript)标准的一部分。然而,一旦装饰器被正式采纳为 ECMAScript 标准的一部分,这些扩展将被提议予以采纳。

🌐 Some examples use the reflect-metadata library which adds a polyfill for an experimental metadata API. This library is not yet part of the ECMAScript (JavaScript) standard. However, once decorators are officially adopted as part of the ECMAScript standard these extensions will be proposed for adoption.

你可以通过 npm 安装这个库:

🌐 You can install this library via npm:

shell
npm i reflect-metadata --save

TypeScript 包含对为带有装饰器的声明生成某些类型元数据的实验性支持。要启用此实验性支持,你必须在命令行或你的 tsconfig.json 中设置 emitDecoratorMetadata 编译器选项:

🌐 TypeScript includes experimental support for emitting certain types of metadata for declarations that have decorators. To enable this experimental support, you must set the emitDecoratorMetadata compiler option either on the command line or in your tsconfig.json:

命令行

shell
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata

tsconfig.json:

启用后,只要已导入 reflect-metadata 库,运行时将会暴露额外的设计时类型信息。

🌐 When enabled, as long as the reflect-metadata library has been imported, additional design-time type information will be exposed at runtime.

我们可以在以下示例中看到这一点:

🌐 We can see this in action in the following example:

ts
import "reflect-metadata";
 
class Point {
constructor(public x: number, public y: number) {}
}
 
class Line {
private _start: Point;
private _end: Point;
 
@validate
set start(value: Point) {
this._start = value;
}
 
get start() {
return this._start;
}
 
@validate
set end(value: Point) {
this._end = value;
}
 
get end() {
return this._end;
}
}
 
function validate<T>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) {
let set = descriptor.set!;
descriptor.set = function (value: T) {
let type = Reflect.getMetadata("design:type", target, propertyKey);
 
if (!(value instanceof type)) {
throw new TypeError(`Invalid type, got ${typeof value} not ${type.name}.`);
}
 
set.call(this, value);
};
}
 
const line = new Line()
line.start = new Point(0, 0)
 
// @ts-ignore
// line.end = {}
 
// Fails at runtime with:
// > Invalid type, got object not Point
 
Try

TypeScript 编译器将使用 @Reflect.metadata 装饰器注入设计时类型信息。你可以将其视为相当于以下 TypeScript:

🌐 The TypeScript compiler will inject design-time type information using the @Reflect.metadata decorator. You could consider it the equivalent of the following TypeScript:

ts
class Line {
private _start: Point;
private _end: Point;
@validate
@Reflect.metadata("design:type", Point)
set start(value: Point) {
this._start = value;
}
get start() {
return this._start;
}
@validate
@Reflect.metadata("design:type", Point)
set end(value: Point) {
this._end = value;
}
get end() {
return this._end;
}
}

注意  装饰器元数据是实验性功能,可能在未来的版本中引入破坏性更改。