注意:本文档涉及实验性第 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"," ": true}}
装饰器
¥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 functionreturn 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:
tsTry
@f @g x
在多行上:
¥On multiple lines:
tsTry
@f @g x
当多个装饰器应用于单个声明时,它们的评估类似于 数学中的函数组合。在此模型中,当复合函数 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 (f ∘ g)(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:
-
每个装饰器的表达式都是从上到下计算的。
¥The expressions for each decorator are evaluated top-to-bottom.
-
然后将结果作为函数从下到上调用。
¥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:
tsTry
functionfirst () {console .log ("first(): factory evaluated");return function (target : any,propertyKey : string,descriptor :PropertyDescriptor ) {console .log ("first(): called");};}functionsecond () {console .log ("second(): factory evaluated");return function (target : any,propertyKey : string,descriptor :PropertyDescriptor ) {console .log ("second(): called");};}classExampleClass {@first ()@second ()method () {}}
这会将这个输出打印到控制台:
¥Which would print this output to the console:
shell
first(): factory evaluatedsecond(): factory evaluatedsecond(): calledfirst(): called
装饰器评价
¥Decorator Evaluation
应用于类内部各种声明的装饰器如何应用有一个明确定义的顺序:
¥There is a well defined order to how decorators applied to various declarations inside of a class are applied:
-
参数装饰器,后跟方法、访问器或属性装饰器应用于每个实例成员。
¥Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each instance member.
-
参数装饰器,后跟方法、访问器或属性装饰器应用于每个静态成员。
¥Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each static member.
-
参数装饰器应用于构造函数。
¥Parameter Decorators are applied for the constructor.
-
类装饰器应用于类。
¥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:
tsTry
@sealed classBugReport {type = "report";title : string;constructor(t : string) {this.title =t ;}}
我们可以使用以下函数声明来定义 @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.
tsTry
functionreportableClassDecorator <T extends { new (...args : any[]): {} }>(constructor :T ) {return class extendsconstructor {reportingURL = "http://www...";};}@reportableClassDecorator classBugReport {type = "report";title : string;constructor(t : string) {this.title =t ;}}constbug = newBugReport ("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:Property 'reportingURL' does not exist on type 'BugReport'.2339Property 'reportingURL' does not exist on type 'BugReport'.bug .; reportingURL
方法装饰器
¥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:
-
静态成员的类的构造函数,或者实例成员的类的原型。
¥Either the constructor function of the class for a static member, or the prototype of the class for an instance member.
-
成员的名称。
¥The name of the member.
-
成员的属性描述符。
¥The Property Descriptor for the member.
注意 如果你的脚本目标小于
ES5
,则属性描述符将为undefined
。¥NOTE The Property Descriptor will be
undefined
if your script target is less thanES5
.
如果方法装饰器返回一个值,它将用作该方法的属性描述符。
¥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:
tsTry
classGreeter {greeting : string;constructor(message : string) {this.greeting =message ;}@enumerable (false)greet () {return "Hello, " + this.greeting ;}}
我们可以使用以下函数声明来定义 @enumerable
装饰器:
¥We can define the @enumerable
decorator using the following function declaration:
tsTry
functionenumerable (value : boolean) {return function (target : any,propertyKey : string,descriptor :PropertyDescriptor ) {descriptor .enumerable =value ;};}
这里的 @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 不允许为单个成员同时修饰
get
和set
访问器。相反,该成员的所有装饰器必须应用于文档顺序中指定的第一个访问器。这是因为装饰器应用于属性描述符,它结合了get
和set
访问器,而不是单独的每个声明。¥NOTE TypeScript disallows decorating both the
get
andset
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 theget
andset
accessor, not each declaration separately.
访问器装饰器的表达式将在运行时作为函数调用,并带有以下三个参数:
¥The expression for the accessor decorator will be called as a function at runtime, with the following three arguments:
-
静态成员的类的构造函数,或者实例成员的类的原型。
¥Either the constructor function of the class for a static member, or the prototype of the class for an instance member.
-
成员的名称。
¥The name of the member.
-
成员的属性描述符。
¥The Property Descriptor for the member.
注意 如果你的脚本目标小于
ES5
,则属性描述符将为undefined
。¥NOTE The Property Descriptor will be
undefined
if your script target is less thanES5
.
如果访问器装饰器返回一个值,它将用作成员的属性描述符。
¥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:
tsTry
classPoint {private_x : number;private_y : number;constructor(x : number,y : number) {this._x =x ;this._y =y ;}@configurable (false)getx () {return this._x ;}@configurable (false)gety () {return this._y ;}}
我们可以使用以下函数声明来定义 @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:
-
静态成员的类的构造函数,或者实例成员的类的原型。
¥Either the constructor function of the class for a static member, or the prototype of the class for an instance member.
-
成员的名称。
¥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 thereflect-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:
-
静态成员的类的构造函数,或者实例成员的类的原型。
¥Either the constructor function of the class for a static member, or the prototype of the class for an instance member.
-
成员的名称。
¥The name of the member.
-
函数参数列表中参数的序号索引。
¥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:
tsTry
classBugReport {type = "report";title : string;constructor(t : string) {this.title =t ;}@validate required verbose : boolean) {if (verbose ) {return `type: ${this.type }\ntitle: ${this.title }`;} else {return this.title ;}}}
然后我们可以使用以下函数声明来定义 @required
和 @validate
装饰器:
¥We can then define the @required
and @validate
decorators using the following function declarations:
tsTry
import "reflect-metadata";constrequiredMetadataKey =Symbol ("required");functionrequired (target :Object ,propertyKey : string | symbol,parameterIndex : number) {letexistingRequiredParameters : number[] =Reflect .getOwnMetadata (requiredMetadataKey ,target ,propertyKey ) || [];existingRequiredParameters .push (parameterIndex );Reflect .defineMetadata (requiredMetadataKey ,existingRequiredParameters ,target ,propertyKey );}functionvalidate (target : any,propertyName : string,descriptor :TypedPropertyDescriptor <Function >) {letmethod =descriptor .value !;descriptor .value = function () {letrequiredParameters : number[] =Reflect .getOwnMetadata (requiredMetadataKey ,target ,propertyName );if (requiredParameters ) {for (letparameterIndex ofrequiredParameters ) {if (parameterIndex >=arguments .length ||arguments [parameterIndex ] ===undefined ) {throw newError ("Missing required argument.");}}}returnmethod .apply (this,arguments );};}
@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 thereflect-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:
{" ": {" ": "ES5"," ": true," ": true}}
启用后,只要导入了 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:
tsTry
import "reflect-metadata";classPoint {constructor(publicx : number, publicy : number) {}}classLine {private_start :Point ;private_end :Point ;@validate setstart (value :Point ) {this._start =value ;}getstart () {return this._start ;}@validate setend (value :Point ) {this._end =value ;}getend () {return this._end ;}}functionvalidate <T >(target : any,propertyKey : string,descriptor :TypedPropertyDescriptor <T >) {letset =descriptor .set !;descriptor .set = function (value :T ) {lettype =Reflect .getMetadata ("design:type",target ,propertyKey );if (!(value instanceoftype )) {throw newTypeError (`Invalid type, got ${typeofvalue } not ${type .name }.`);}set .call (this,value );};}constline = newLine ()line .start = newPoint (0, 0)// @ts-ignore// line.end = {}// Fails at runtime with:// > Invalid type, got object not Point
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.