装饰器
注意 本文档指的是实验性的阶段2装饰器实现。自 TypeScript 5.0 起,阶段3装饰器支持已可用。 参见:TypeScript 5.0 中的装饰器
引言
随着 TypeScript 和 ES6 中类的引入,现在存在某些场景需要额外特性来支持注解或修改类及其成员。 装饰器提供了一种为类声明和成员添加注解以及元编程语法的方式。
进一步阅读(阶段2):TypeScript 装饰器完全指南
要启用装饰器的实验性支持,你必须在命令行或 tsconfig.json 中启用 experimentalDecorators 编译器选项:
命令行:
tsc --target ES5 --experimentalDecoratorstsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}装饰器
装饰器是一种特殊类型的声明,它可以附加到类声明、方法、访问器、属性或参数上。 装饰器使用 @expression 的形式,其中 expression 必须求值为一个函数,该函数将在运行时被调用,并带有被装饰声明的相关信息。
例如,给定装饰器 @sealed,我们可以如下编写 sealed 函数:
function sealed(target) {
// 对 'target' 进行操作...
}装饰器工厂
如果我们想要定制装饰器如何应用于声明,我们可以编写一个装饰器工厂。 装饰器工厂就是一个返回表达式的函数,该表达式将在运行时被装饰器调用。
我们可以按以下方式编写装饰器工厂:
function color(value: string) {
// 这是装饰器工厂,它设置了
// 返回的装饰器函数
return function (target) {
// 这是装饰器
// 对 'target' 和 'value' 进行操作...
};
}装饰器组合
多个装饰器可以应用于一个声明,例如在同一行:
在多行上:
当多个装饰器应用于单个声明时,它们的求值类似于数学中的函数组合。在此模型中,组合函数 f 和 g 时,得到的复合函数 (f ∘ g)(x) 等价于 f(g(x))。
因此,在 TypeScript 中对单个声明上的多个装饰器求值时,会执行以下步骤:
- 每个装饰器的表达式自上而下求值。
- 然后将结果作为函数自下而上调用。
如果我们使用装饰器工厂,可以通过以下示例观察到此求值顺序:
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这将在控制台打印以下输出:
first(): factory evaluated
second(): factory evaluated
second(): called
first(): called装饰器求值
对于类中各种声明的装饰器,其应用顺序是有明确定义的:
- 对每个实例成员,先应用参数装饰器,然后是方法、访问器或属性装饰器。
- 对每个静态成员,先应用参数装饰器,然后是方法、访问器或属性装饰器。
- 参数装饰器应用于构造函数。
- 类装饰器应用于类。
类装饰器
类装饰器在类声明之前声明。 类装饰器应用于类的构造函数,可用于观察、修改或替换类定义。 类装饰器不能用于声明文件或任何其他环境上下文(例如在 declare 类上)。
类装饰器的表达式将在运行时作为函数调用,被装饰类的构造函数是其唯一参数。
如果类装饰器返回一个值,它将用提供的构造函数替换类声明。
注意 如果你选择返回一个新的构造函数,你必须注意维护原始的原型。 在运行时应用装饰器的逻辑不会为你做这件事。
以下是一个应用于 BugReport 类的类装饰器(@sealed)的示例:
@sealed
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}Try我们可以使用以下函数声明来定义 @sealed 装饰器:
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}当 @sealed 执行时,它将密封构造函数及其原型,因此将通过访问 BugReport.prototype 或在 BugReport 本身上定义属性,来防止在运行时向此类添加或删除任何进一步的功能(请注意,ES2015 类实际上只是基于原型的构造函数语法的语法糖)。此装饰器不会阻止类子类化 BugReport。
接下来,我们有一个如何覆盖构造函数以设置新默认值的示例。
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); // 打印 "Needs dark mode"
console.log(bug.type); // 打印 "report"
// 注意装饰器_不会_改变 TypeScript 类型
// 因此新属性 `reportingURL` 对类型系统未知:
bug.reportingURL;Try方法装饰器
方法装饰器在方法声明之前声明。 装饰器应用于方法的属性描述符,可用于观察、修改或替换方法定义。 方法装饰器不能用于声明文件、重载或任何其他环境上下文(例如在 declare 类中)。
方法装饰器的表达式将在运行时作为函数调用,带有以下三个参数:
- 对于静态成员,是类的构造函数;对于实例成员,是类的原型。
- 成员的名称。
- 成员的属性描述符。
注意 如果你的脚本目标低于
ES5,则属性描述符将为undefined。
如果方法装饰器返回一个值,它将用作方法的属性描述符。
注意 如果你的脚本目标低于
ES5,则返回值将被忽略。
以下是一个应用于 Greeter 类上方法的方法装饰器(@enumerable)的示例:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}Try我们可以使用以下函数声明来定义 @enumerable 装饰器:
function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}Try这里的 @enumerable(false) 装饰器是一个装饰器工厂。 当调用 @enumerable(false) 装饰器时,它会修改属性描述符的 enumerable 属性。
访问器装饰器
访问器装饰器在访问器声明之前声明。 访问器装饰器应用于访问器的属性描述符,可用于观察、修改或替换访问器的定义。 访问器装饰器不能用于声明文件或任何其他环境上下文(例如在 declare 类中)。
注意 TypeScript 不允许同时装饰单个成员的
get和set访问器。 相反,该成员的所有装饰器必须应用于文档顺序中指定的第一个访问器。 这是因为装饰器应用于属性描述符,它结合了get和set访问器,而不是分别应用于每个声明。
访问器装饰器的表达式将在运行时作为函数调用,带有以下三个参数:
- 对于静态成员,是类的构造函数;对于实例成员,是类的原型。
- 成员的名称。
- 成员的属性描述符。
注意 如果你的脚本目标低于
ES5,则属性描述符将为undefined。
如果访问器装饰器返回一个值,它将用作成员的属性描述符。
注意 如果你的脚本目标低于
ES5,则返回值将被忽略。
以下是一个应用于 Point 类成员上的访问器装饰器(@configurable)的示例:
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 装饰器:
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}属性装饰器
属性装饰器在属性声明之前声明。 属性装饰器不能用于声明文件或任何其他环境上下文(例如在 declare 类中)。
属性装饰器的表达式将在运行时作为函数调用,带有以下两个参数:
- 对于静态成员,是类的构造函数;对于实例成员,是类的原型。
- 成员的名称。
注意 由于属性装饰器在 TypeScript 中的初始化方式,属性描述符不作为参数提供给属性装饰器。 这是因为目前没有机制可以在定义原型成员时描述实例属性,也没有办法观察或修改属性的初始化器。返回值也被忽略。 因此,属性装饰器只能用于观察类中已声明了特定名称的属性。
我们可以使用此信息来记录关于属性的元数据,如下例所示:
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 函数:
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 时,它会读取该格式的元数据值。
注意 此示例需要
reflect-metadata库。 有关reflect-metadata库的更多信息,请参阅元数据。
参数装饰器
参数装饰器在参数声明之前声明。 参数装饰器应用于类构造函数或方法声明的函数。 参数装饰器不能用于声明文件、重载或任何其他环境上下文(例如在 declare 类中)。
参数装饰器的表达式将在运行时作为函数调用,带有以下三个参数:
- 对于静态成员,是类的构造函数;对于实例成员,是类的原型。
- 成员的名称。
- 参数在函数参数列表中的序号索引。
注意 参数装饰器只能用于观察方法上已声明了一个参数。
参数装饰器的返回值被忽略。
以下是一个应用于 BugReport 类成员参数上的参数装饰器(@required)的示例:
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 装饰器:
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 方法包装在一个函数中,该函数在调用原始方法之前验证参数。
注意 此示例需要
reflect-metadata库。 有关reflect-metadata库的更多信息,请参阅元数据。
元数据
一些示例使用了 reflect-metadata 库,它为实验性元数据 API 添加了一个 polyfill。 该库尚不属于 ECMAScript(JavaScript)标准的一部分。 然而,一旦装饰器被正式采纳为 ECMAScript 标准的一部分,这些扩展将被提议采纳。
你可以通过 npm 安装此库:
npm i reflect-metadata --saveTypeScript 包含对为带有装饰器的声明发出某些类型元数据的实验性支持。 要启用此实验性支持,你必须在命令行或 tsconfig.json 中设置 emitDecoratorMetadata 编译器选项:
命令行:
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadatatsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}启用后,只要导入了 reflect-metadata 库,额外的设计时类型信息将在运行时暴露。
我们可以在以下示例中看到这一点:
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 = {}
// 运行时失败:
// > Invalid type, got object not PointTryTypeScript 编译器将使用 @Reflect.metadata 装饰器注入设计时类型信息。 你可以将其视为以下 TypeScript 的等价物:
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;
}
}注意 装饰器元数据是一个实验性功能,可能在未来的版本中引入破坏性更改。