装饰器

注意:本文档涉及实验性第 2 阶段装饰器实现。从 Typescript 5.0 开始提供第 3 阶段装饰器支持。查看:Typescript 5.0 中的装饰器

¥NOTE  This document refers to an experimental stage 2 decorators implementation. Stage 3 decorator support is available since Typescript 5.0. See: Decorators in 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.

进一步阅读(第二阶段):TypeScript 装饰器的完整指南

¥Further Reading (stage 2): A Complete Guide to TypeScript Decorators

要启用对装饰器的实验性支持,你必须在命令行或 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:

命令行:

¥Command Line:

shell
tsc --target ES5 --experimentalDecorators

tsconfig.json:

{
"": "ES5",
}
}

装饰器

¥Decorators

装饰器是一种特殊的声明,可以附加到 类声明方法accessor属性参数。装饰器使用 @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

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

¥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

当多个装饰器应用于单个声明时,它们的评估类似于 数学中的函数组合。在此模型中,当复合函数 f 和 g 时,得到的复合 (f ∘ g)(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. 每个装饰器的表达式都是从上到下计算的。

    ¥The expressions for each decorator are evaluated top-to-bottom.

  2. 然后将结果作为函数从下到上调用。

    ¥The results are then called as functions from bottom-to-top.

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

¥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. 参数装饰器,后跟方法、访问器或属性装饰器应用于每个实例成员。

    ¥Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each instance member.

  2. 参数装饰器,后跟方法、访问器或属性装饰器应用于每个静态成员。

    ¥Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each static member.

  3. 参数装饰器应用于构造函数。

    ¥Parameter Decorators are applied for the constructor.

  4. 类装饰器应用于类。

    ¥Class Decorators are applied for the class.

类装饰器

¥Class Decorators

类装饰器是在类声明之前声明的。类装饰器应用于类的构造函数,可用于观察、修改或替换类定义。类装饰器不能在声明文件或任何其他环境上下文中使用(例如在 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.

注意:如果你选择返回新的构造函数,则必须注意维护原始原型。在运行时应用装饰器的逻辑不会为你执行此操作。

¥NOTE  Should you choose to return a new constructor function, you must take care to maintain the original prototype. The logic that applies decorators at runtime will not do this for you.

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

¥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

方法装饰器在方法声明之前声明。装饰器应用于方法的属性描述符,可用于观察、修改或替换方法定义。方法装饰器不能用于声明文件、重载或任何其他环境上下文(例如在 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. 静态成员的类的构造函数,或者实例成员的类的原型。

    ¥Either the constructor function of the class for a static member, or the prototype of the class for an instance member.

  2. 成员的名称。

    ¥The name of the member.

  3. 成员的属性描述符。

    ¥The Property Descriptor for the member.

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

¥NOTE  The Property Descriptor will be undefined if your script target is less than ES5.

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

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

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

¥NOTE  The return value is ignored if your script target is less than ES5.

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

¥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 访问器,而不是单独的每个声明。

¥NOTE  TypeScript disallows decorating both the get and set accessor for a single member. Instead, all decorators for the member must be applied to the first accessor specified in document order. This is because decorators apply to a Property Descriptor, which combines both the get and set accessor, not each declaration separately.

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

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

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

    ¥Either the constructor function of the class for a static member, or the prototype of the class for an instance member.

  2. 成员的名称。

    ¥The name of the member.

  3. 成员的属性描述符。

    ¥The Property Descriptor for the member.

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

¥NOTE  The Property Descriptor will be undefined if your script target is less than ES5.

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

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

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

¥NOTE  The return value is ignored if your script target is less than ES5.

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

¥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. 静态成员的类的构造函数,或者实例成员的类的原型。

    ¥Either the constructor function of the class for a static member, or the prototype of the class for an instance member.

  2. 成员的名称。

    ¥The name of the member.

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

¥NOTE  A Property Descriptor is not provided as an argument to a property decorator due to how property decorators are initialized in TypeScript. This is because there is currently no mechanism to describe an instance property when defining members of a prototype, and no way to observe or modify the initializer for a property. The return value is ignored too. As such, a property decorator can only be used to observe that a property of a specific name has been declared for a class.

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

¥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 库的更多信息,请参阅 元数据

¥NOTE  This example requires the reflect-metadata library. See Metadata for more information about the reflect-metadata library.

参数装饰器

¥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. 静态成员的类的构造函数,或者实例成员的类的原型。

    ¥Either the constructor function of the class for a static member, or the prototype of the class for an instance member.

  2. 成员的名称。

    ¥The name of the member.

  3. 函数参数列表中参数的序号索引。

    ¥The ordinal index of the parameter in the function’s parameter list.

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

¥NOTE  A parameter decorator can only be used to observe that a parameter has been declared on a method.

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

¥The return value of the parameter decorator is ignored.

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

¥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 库的更多信息,请参阅 元数据

¥NOTE  This example requires the reflect-metadata library. See Metadata for more information about the reflect-metadata library.

元数据

¥Metadata

一些示例使用为 实验性元数据 API 添加 polyfill 的 reflect-metadata 库。这个库还不是 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:

命令行:

¥Command Line:

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

注意 装饰器元数据是一项实验性功能,可能会在未来版本中引入重大更改。

¥NOTE  Decorator metadata is an experimental feature and may introduce breaking changes in future releases.