装饰器

注意:本文件指的是实验阶段 2 装饰器实现。从 Typescript 5.0 开始,支持阶段 3 装饰器。参见:Typescript 5.0 中的装饰器

介绍

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

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

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

命令行:

shell
tsc --target ES5 --experimentalDecorators

tsconfig.json:

{
"": "ES5",
}
}

装饰器

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

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

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

装饰器工厂

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

我们可以按以下方式编写装饰器工厂

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

装饰器组合

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

ts
@f @g x
Try

在多行上

ts
@f
@g
x
Try

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

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

  1. 每个装饰器的表达式从上到下进行评估。
  2. 然后从下到上将结果作为函数调用。

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

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

这将打印以下输出到控制台

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

装饰器评估

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

  1. 对于每个实例成员,应用参数装饰器,然后应用方法访问器属性装饰器
  2. 对于每个静态成员,应用参数装饰器,然后应用方法访问器属性装饰器
  3. 对于构造函数,应用参数装饰器
  4. 对于类,应用类装饰器

类装饰器

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

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

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

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

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

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

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

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

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

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

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

方法装饰器

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

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

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

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

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

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

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

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

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

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

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

访问器装饰器

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

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

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

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

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

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

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

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

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装饰器

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

属性装饰器

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

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

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

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

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

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函数

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时,它读取格式的元数据值。

注意 此示例需要reflect-metadata库。有关reflect-metadata库的更多信息,请参阅元数据

参数装饰器

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

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

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

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

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

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

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 装饰器

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 方法包装在一个函数中,该函数在调用原始方法之前验证参数。

注意 此示例需要reflect-metadata库。有关reflect-metadata库的更多信息,请参阅元数据

元数据

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

您可以通过 npm 安装此库

shell
npm i reflect-metadata --save

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

命令行:

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

tsconfig.json:

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

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

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 的等效项

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

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

TypeScript 文档是一个开源项目。帮助我们改进这些页面 通过发送 Pull Request

此页面的贡献者
RBRon Buckton (54)
OTOrta Therox (15)
MHMohamed Hegazy (3)
DRDinanjanan Ravindran (2)
HAHossein Ahmadian-Yazdi (2)
22+

上次更新时间:2024 年 3 月 21 日