Skip to content
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

装饰器

注意 本文档指的是实验性的阶段2装饰器实现。自 TypeScript 5.0 起,阶段3装饰器支持已可用。 参见:TypeScript 5.0 中的装饰器

引言

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

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

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

命令行

shell
tsc --target ES5 --experimentalDecorators

tsconfig.json

json
{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true
  }
}

装饰器

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

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

ts
function sealed(target) {
  // 对 'target' 进行操作...
}

装饰器工厂

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

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

ts
function color(value: string) {
  // 这是装饰器工厂,它设置了
  // 返回的装饰器函数
  return function (target) {
    // 这是装饰器
    // 对 'target' 和 '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
); // 打印 "Needs dark mode"
console
.
log
(
bug
.
type
); // 打印 "report"
// 注意装饰器_不会_改变 TypeScript 类型 // 因此新属性 `reportingURL` 对类型系统未知:
bug
.reportingURL;
Property 'reportingURL' does not exist on type 'BugReport'.
Try

方法装饰器

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

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

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

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

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

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

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

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,则返回值将被忽略。

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

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. 参数在函数参数列表中的序号索引。

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

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

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

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

json
{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

启用后,只要导入了 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 = {} // 运行时失败: // > 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;
  }
}

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