注意:本文件指的是實驗階段 2 的裝飾器實作。自 TypeScript 5.0 起提供階段 3 裝飾器支援。請參閱:TypeScript 5.0 中的裝飾器
簡介
隨著 TypeScript 和 ES6 中的類別引入,現在存在某些情況需要額外功能來支援註解或修改類別和類別成員。裝飾器提供一種方法,可以為類別宣告和成員新增註解和元程式設計語法。
進一步閱讀(階段 2):TypeScript 裝飾器的完整指南
若要啟用裝飾器的實驗支援,您必須在命令列或您的 tsconfig.json
中啟用 experimentalDecorators
編譯器選項
命令列:
shell
tsc --target ES5 --experimentalDecorators
tsconfig.json:
{" ": {" ": "ES5"," ": true}}
裝飾器
裝飾器是一種特殊類型的宣告,可以附加到 類別宣告、方法、存取器、屬性 或 參數。裝飾器使用 @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 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 中針對單一宣告評估多個裝飾器時,會執行下列步驟
- 自上而下評估每個裝飾器的表達式。
- 然後自下而上將結果呼叫為函數。
如果我們要使用 裝飾器工廠,我們可以使用以下範例觀察此評估順序
tsTry
functionfirst () {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 () {}}
這會將此輸出列印至主控台
shell
first(): factory evaluatedsecond(): factory evaluatedsecond(): calledfirst(): called
裝飾器評估
套用至類別內部各種宣告的裝飾器,其套用順序有明確的定義
- 參數裝飾器,接著是 方法、存取器或 屬性裝飾器,會套用至每個執行個體成員。
- 參數裝飾器,接著是 方法、存取器或 屬性裝飾器,會套用至每個靜態成員。
- 參數裝飾器會套用至建構函數。
- 類別裝飾器會套用至類別。
類別裝飾器
類別裝飾器會在類別宣告之前宣告。類別裝飾器會套用至類別的建構函數,可用於觀察、修改或取代類別定義。類別裝飾器不能用於宣告檔案,或任何其他環境背景(例如在 declare
類別上)。
類別裝飾器的表達式會在執行階段以函數形式呼叫,並將已裝飾類別的建構函數作為其唯一引數。
如果類別裝飾器傳回值,它會以提供的建構函數取代類別宣告。
注意 如果您選擇傳回新的建構函數,您必須注意維護原始原型。在執行階段套用裝飾器的邏輯不會為您執行此操作。
以下是套用至 BugReport
類別的類別裝飾器 (@sealed
) 範例
tsTry
@sealed classBugReport {type = "report";title : string;constructor(t : string) {this.title =t ;}}
我們可以使用下列函式宣告來定義 @sealed
裝飾器
ts
function sealed(constructor: Function) {Object.seal(constructor);Object.seal(constructor.prototype);}
當執行 @sealed
時,它會封裝建構函式及其原型,因此會防止在執行期間透過存取 BugReport.prototype
或定義 BugReport
本身的屬性來新增或移除此類別的任何進一步功能(請注意,ES2015 類別實際上只是原型化建構函式的語法糖)。此裝飾器不會防止類別對 BugReport
進行子類別化。
接下來,我們有一個範例說明如何覆寫建構函式以設定新的預設值。
tsTry
functionreportableClassDecorator <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
,則會略過傳回值。
以下是套用在 Greeter
類別方法上的方法裝飾器 (@enumerable
) 範例
tsTry
classGreeter {greeting : string;constructor(message : string) {this.greeting =message ;}@enumerable (false)greet () {return "Hello, " + this.greeting ;}}
我們可以使用下列函式宣告來定義 @enumerable
裝飾器
tsTry
functionenumerable (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
,則會略過傳回值。
以下是套用至Point
類別成員的存取器裝飾器(@configurable
)範例
tsTry
classPoint {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
裝飾器
ts
function configurable(value: boolean) {return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {descriptor.configurable = value;};}
屬性裝飾器
屬性裝飾器在屬性宣告之前宣告。屬性裝飾器不能用於宣告檔案,或任何其他環境脈絡(例如在declare
類別中)。
屬性裝飾器的表示式會在執行階段作為函式呼叫,並具有下列兩個引數
- 靜態成員的類別建構函式,或實例成員的類別原型。
- 成員的名稱。
注意 由於屬性裝飾器在 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
類別中)。
參數裝飾器的表達式將在執行階段以函式呼叫,並具有下列三個引數
- 靜態成員的類別建構函式,或實例成員的類別原型。
- 成員的名稱。
- 函式參數清單中參數的序數索引。
注意 參數裝飾器只能用來觀察方法上已宣告的參數。
參數裝飾器的傳回值將被忽略。
以下是一個參數裝飾器 (@required
) 套用在 BugReport
類別成員參數的範例
tsTry
classBugReport {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
裝飾器
tsTry
import "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 的多載。這個函式庫尚未成為 ECMAScript (JavaScript) 標準的一部分。不過,一旦裝飾器正式採用為 ECMAScript 標準的一部分,這些延伸功能就會被提議採用。
您可以透過 npm 安裝這個函式庫
shell
npm i reflect-metadata --save
TypeScript 包含對發出具有裝飾器的宣告的特定元資料類型的實驗性支援。若要啟用這個實驗性支援,您必須在命令列或 tsconfig.json
中設定 emitDecoratorMetadata
編譯器選項
命令列:
shell
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
tsconfig.json:
{" ": {" ": "ES5"," ": true," ": true}}
啟用後,只要已匯入 reflect-metadata
函式庫,就會在執行階段公開額外的設計時期類型資訊。
我們可以在以下範例中看到這個動作
tsTry
import "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
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;}}
注意 裝飾器元資料是一項實驗性功能,可能會在未來的版本中引入重大變更。