裝飾器

注意:本文件指的是實驗階段 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' ...
}

裝飾器工廠

如果我們想要自訂裝飾器如何套用至宣告,我們可以撰寫一個裝飾器工廠。裝飾器工廠只是一個函式,它會傳回裝飾器在執行時會呼叫的 expression。

我們可以以下列方式撰寫裝飾器工廠

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,則會略過傳回值。

以下是套用在 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. 函式參數清單中參數的序數索引。

注意 參數裝飾器只能用來觀察方法上已宣告的參數。

參數裝飾器的傳回值將被忽略。

以下是一個參數裝飾器 (@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 的多載。這個函式庫尚未成為 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 日