注意:本文档涉及实验性的第 2 阶段(Stage 2)装饰器实现。TypeScript 5.0 起支持第 3 阶段(Stage 3)装饰器。请参阅:TypeScript 5.0 中的装饰器
介绍
随着 TypeScript 和 ES6 中类的引入,现在存在某些需要附加功能来支持注释或修改类及类成员的场景。装饰器提供了一种为类声明和成员添加注释和元编程语法的方法。
延伸阅读(第 2 阶段):TypeScript 装饰器完全指南
要启用对装饰器的实验性支持,您必须在命令行或 tsconfig.json 中启用 experimentalDecorators 编译器选项。
命令行:
shelltsc --target ES5 --experimentalDecorators
tsconfig.json:
{"": {"": "ES5","": true}}
装饰器
装饰器是一种特殊类型的声明,可以附加到类声明、方法、访问器、属性或参数上。装饰器使用 @expression 形式,其中 expression 必须求值为一个函数,该函数会在运行时被调用,并带有关于被装饰声明的信息。
例如,给定 @sealed 装饰器,我们可以按照以下方式编写 sealed 函数:
tsfunction sealed(target) {// do something with 'target' ...}
装饰器工厂
如果我们想要自定义装饰器如何应用于声明,我们可以编写一个装饰器工厂。装饰器工厂只是一个返回表达式的函数,该表达式将在运行时由装饰器调用。
我们可以通过以下方式编写装饰器工厂:
tsfunction 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'...};}
装饰器组合
可以在一个声明上应用多个装饰器,例如在同一行:
tsTry@f @g x
在多行上:
tsTry@f @g x
当多个装饰器应用于单个声明时,它们的求值类似于数学中的函数组合。在这种模型中,当组合函数 f 和 g 时,得到的组合 (f ∘ g)(x) 等价于 f(g(x))。
因此,在 TypeScript 中对单个声明求值多个装饰器时,会执行以下步骤:
- 每个装饰器的表达式按从上到下的顺序求值。
- 然后,求值结果按从下到上的顺序作为函数调用。
如果我们使用装饰器工厂,我们可以通过以下示例观察此求值顺序:
tsTryfunctionfirst () {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 () {}}
这会在控制台中打印以下输出:
shellfirst(): factory evaluatedsecond(): factory evaluatedsecond(): calledfirst(): called
装饰器求值
类内部应用到各种声明的装饰器具有定义明确的应用顺序:
- 参数装饰器,然后是每个实例成员的方法、访问器或属性装饰器。
- 参数装饰器,然后是每个静态成员的方法、访问器或属性装饰器。
- 参数装饰器应用于构造函数。
- 类装饰器应用于类。
类装饰器
类装饰器在类声明之前声明。类装饰器应用于类的构造函数,可以用来监视、修改或替换类定义。类装饰器不能在声明文件或任何其他环境上下文(例如 declare 类)中使用。
类装饰器的表达式将在运行时作为函数调用,并将被装饰类的构造函数作为其唯一参数。
如果类装饰器返回一个值,它将使用提供的构造函数替换类声明。
注意:如果您选择返回一个新的构造函数,必须小心维护原始原型。在运行时应用装饰器的逻辑不会为您自动处理。
以下是将类装饰器 (@sealed) 应用于 BugReport 类的示例:
tsTry@sealed classBugReport {type = "report";title : string;constructor(t : string) {this.title =t ;}}
我们可以使用以下函数声明定义 @sealed 装饰器:
tsfunction sealed(constructor: Function) {Object.seal(constructor);Object.seal(constructor.prototype);}
当 @sealed 执行时,它会密封构造函数及其原型,因此在运行时通过访问 BugReport.prototype 或在 BugReport 本身上定义属性,将无法再向该类添加或删除任何功能(请注意,ES2015 类实际上只是基于原型的构造函数的语法糖)。此装饰器不会阻止类继承 BugReport。
接下来是一个如何覆盖构造函数以设置新默认值的示例。
tsTryfunctionreportableClassDecorator <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
方法装饰器
方法装饰器在方法声明之前声明。装饰器应用于方法的属性描述符,可以用来监视、修改或替换方法定义。方法装饰器不能在声明文件、重载或任何其他环境上下文(例如 declare 类)中使用。
方法装饰器的表达式将在运行时作为函数调用,带有以下三个参数:
- 对于静态成员,是类的构造函数;对于实例成员,是类的原型。
- 成员的名称。
- 成员的属性描述符。
注意:如果您的脚本目标低于
ES5,属性描述符将为undefined。
如果方法装饰器返回一个值,它将被用作该方法的属性描述符。
注意:如果您的脚本目标低于
ES5,返回值将被忽略。
以下是将方法装饰器 (@enumerable) 应用于 Greeter 类方法的示例:
tsTryclassGreeter {greeting : string;constructor(message : string) {this.greeting =message ;}@enumerable (false)greet () {return "Hello, " + this.greeting ;}}
我们可以使用以下函数声明定义 @enumerable 装饰器:
tsTryfunctionenumerable (value : boolean) {return function (target : any,propertyKey : string,descriptor :PropertyDescriptor ) {descriptor .enumerable =value ;};}
这里的 @enumerable(false) 装饰器是一个装饰器工厂。当调用 @enumerable(false) 时,它会修改属性描述符的 enumerable 属性。
访问器装饰器
访问器装饰器在访问器声明之前声明。访问器装饰器应用于访问器的属性描述符,可以用来监视、修改或替换访问器的定义。访问器装饰器不能在声明文件或任何其他环境上下文(例如 declare 类)中使用。
注意:TypeScript 不允许为单个成员同时装饰
get和set访问器。相反,该成员的所有装饰器必须应用到文档顺序中指定的第一个访问器上。这是因为装饰器应用于属性描述符,它结合了get和set访问器,而不是分别装饰每个声明。
访问器装饰器的表达式将在运行时作为函数调用,带有以下三个参数:
- 对于静态成员,是类的构造函数;对于实例成员,是类的原型。
- 成员的名称。
- 成员的属性描述符。
注意:如果您的脚本目标低于
ES5,属性描述符将为undefined。
如果访问器装饰器返回一个值,它将被用作该成员的属性描述符。
注意:如果您的脚本目标低于
ES5,返回值将被忽略。
以下是将访问器装饰器 (@configurable) 应用于 Point 类成员的示例:
tsTryclassPoint {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 装饰器:
tsfunction configurable(value: boolean) {return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {descriptor.configurable = value;};}
属性装饰器
属性装饰器在属性声明之前声明。属性装饰器不能在声明文件或任何其他环境上下文(例如 declare 类)中使用。
属性装饰器的表达式将在运行时作为函数调用,带有以下两个参数:
- 对于静态成员,是类的构造函数;对于实例成员,是类的原型。
- 成员的名称。
注意:由于 TypeScript 初始化属性装饰器的方式,属性装饰器不会提供属性描述符作为参数。这是因为目前还没有机制可以在定义原型成员时描述实例属性,也没有办法监视或修改属性的初始化程序。返回值也会被忽略。因此,属性装饰器只能用于监视类是否声明了特定名称的属性。
我们可以利用这些信息记录关于属性的元数据,如下例所示:
tsclass 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 函数:
tsimport "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 时,它会读取格式的元数据值。
注意:此示例需要
reflect-metadata库。有关reflect-metadata库的更多信息,请参阅元数据。
参数装饰器
参数装饰器在参数声明之前声明。参数装饰器应用于类构造函数或方法声明的函数。参数装饰器不能在声明文件、重载或任何其他环境上下文(例如 declare 类)中使用。
参数装饰器的表达式将在运行时作为函数调用,带有以下三个参数:
- 对于静态成员,是类的构造函数;对于实例成员,是类的原型。
- 成员的名称。
- 函数参数列表中参数的序号索引。
注意:参数装饰器只能用于监视方法上是否声明了参数。
参数装饰器的返回值将被忽略。
以下是将参数装饰器 (@required) 应用于 BugReport 类成员参数的示例:
tsTryclassBugReport {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 装饰器:
tsTryimport "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 方法包装在一个函数中,该函数在调用原始方法之前验证参数。
注意:此示例需要
reflect-metadata库。有关reflect-metadata库的更多信息,请参阅元数据。
元数据
一些示例使用了 reflect-metadata 库,它为实验性元数据 API 添加了一个 polyfill。此库尚不是 ECMAScript (JavaScript) 标准的一部分。然而,一旦装饰器被正式采纳为 ECMAScript 标准的一部分,这些扩展将被提议采纳。
您可以通过 npm 安装此库:
shellnpm i reflect-metadata --save
TypeScript 包含对为具有装饰器的声明发出某些类型元数据的实验性支持。要启用此实验性支持,您必须在命令行或 tsconfig.json 中设置 emitDecoratorMetadata 编译器选项。
命令行:
shelltsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
tsconfig.json:
{"": {"": "ES5","": true,"": true}}
启用后,只要导入了 reflect-metadata 库,额外的设计时类型信息就会在运行时暴露出来。
我们可以在以下示例中看到其实际效果:
tsTryimport "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 代码:
tsclass 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;}}
注意:装饰器元数据是一项实验性功能,未来版本中可能会引入重大变更。