本文 AI 產出,尚未審核
TypeScript 裝飾器(Decorators)—— 基本語法與實務應用
簡介
在大型前端或 Node.js 專案中,維護性、可讀性 與 可重用性 常常是開發團隊最關心的議題。
TypeScript 的裝飾器(Decorator)正是一把可以讓程式碼更具表意、同時將橫切關注點(cross‑cutting concerns)抽離出來的利器。透過簡潔的語法,我們能在類別、屬性、方法、參數甚至存取子(accessor)上「貼標籤」,在執行時自動注入額外行為。
本篇文章將從 語法、執行順序、實作範例 逐步說明裝飾器的核心概念,並提供 常見陷阱 與 最佳實踐,協助讀者在日常開發中安全、有效地運用裝飾器。
核心概念
1. 裝飾器的類型與使用時機
| 裝飾器類型 | 作用對象 | 呼叫時機 |
|---|---|---|
| Class Decorator | 類別本身 | 類別宣告完成後 |
| Property Decorator | 類別屬性 | 屬性定義時 |
| Method Decorator | 類別方法 | 方法定義時 |
| Accessor Decorator | getter / setter | 存取子定義時 |
| Parameter Decorator | 方法參數 | 參數列表解析時 |
註:所有裝飾器本質上都是 函式,接受不同的參數,返回值(若有)會影響原始宣告的行為。
2. 啟用裝飾器的編譯設定
在 tsconfig.json 中必須開啟下列選項:
{
"compilerOptions": {
"experimentalDecorators": true, // 允許使用裝飾器語法
"emitDecoratorMetadata": true // (可選)產生型別資訊供 DI 框架使用
}
}
⚠️
experimentalDecorators為實驗性功能,未來可能會有變動;目前仍是正式發布的語法。
3. 基本語法範例
以下示範最簡單的 類別裝飾器,它會在類別被建立時印出一段訊息。
function LogClass(target: Function) {
console.log(`類別 ${target.name} 已被建立`);
}
@LogClass
class Person {
constructor(public name: string) {}
}
執行結果:
類別 Person 已被建立
3.1 方法裝飾器:攔截與修改行為
function LogMethod(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value; // 保存原始方法
descriptor.value = function (...args: any[]) {
console.log(`呼叫 ${propertyKey},參數:`, args);
const result = original.apply(this, args); // 執行原始方法
console.log(`結果:`, result);
return result;
};
return descriptor;
}
class Calculator {
@LogMethod
add(a: number, b: number) {
return a + b;
}
}
new Calculator().add(2, 3);
// 輸出:
// 呼叫 add,參數: [2,3]
// 結果: 5
3.2 屬性裝飾器:自動綁定 this
在 UI 框架(如 Angular)中,常需要把事件處理函式綁定到正確的 this。以下示範一個 自動綁定 的屬性裝飾器:
function Autobind(_: any, _2: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
const adjDescriptor: PropertyDescriptor = {
configurable: true,
get() {
return originalMethod.bind(this);
},
};
return adjDescriptor;
}
class Button {
message = "點擊了!";
@Autobind
handleClick() {
console.log(this.message);
}
}
const btn = new Button();
const handler = btn.handleClick; // 失去原本的 this
handler(); // 正確印出 "點擊了!"
3.3 參數裝飾器:收集型別資訊(配合 emitDecoratorMetadata)
function Required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
const existingRequiredParameters: number[] =
Reflect.getOwnMetadata('required:param', target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata('required:param', existingRequiredParameters, target, propertyKey);
}
class Service {
greet(@Required name: string) {
console.log(`Hello, ${name}`);
}
}
以上範例需要安裝
reflect-metadata並在入口檔案import "reflect-metadata";。
3.4 存取子裝飾器:驗證 Setter
function MinLength(length: number) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const set = descriptor.set!;
descriptor.set = function (value: string) {
if (value.length < length) {
throw new Error(`${propertyKey} 必須至少 ${length} 個字元`);
}
set.call(this, value);
};
};
}
class User {
private _username = '';
@MinLength(4)
set username(value: string) {
this._username = value;
}
get username() {
return this._username;
}
}
const u = new User();
u.username = 'Tom'; // Error: username 必須至少 4 個字元
3.5 組合使用:類別 + 方法
function Controller(prefix: string) {
return function (target: Function) {
Reflect.defineMetadata('prefix', prefix, target);
};
}
function Get(path: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const routes = Reflect.getMetadata('routes', target.constructor) || [];
routes.push({ method: 'GET', path, handler: descriptor.value });
Reflect.defineMetadata('routes', routes, target.constructor);
};
}
@Controller('/api')
class ApiController {
@Get('/users')
getUsers() {
return ['Alice', 'Bob'];
}
}
此模式常見於 NestJS、routing‑controllers 等框架,展示了裝飾器在 元資料(metadata)與 路由映射 上的威力。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
忘記開啟 experimentalDecorators |
編譯會直接報錯 Experimental support for decorators is a feature that is subject to change |
確認 tsconfig.json 中已設定 experimentalDecorators: true |
| 裝飾器執行順序不明 | 多層裝飾器會依「從下往上」的順序執行(先執行最內層) | 以簡單的 console.log 測試順序,或閱讀官方文件的執行圖表 |
| 返回值錯誤 | 方法或屬性裝飾器若未返回正確的 PropertyDescriptor,可能導致行為失效 |
始終 return descriptor;(或修改後的 descriptor) |
與 this 綁定衝突 |
裝飾器內部若自行 bind,可能覆寫其他裝飾器的綁定 |
使用 Autobind 類型的裝飾器時,確保只套用一次 |
| 過度使用導致程式碼難以追蹤 | 裝飾器過多會讓執行流程變得不透明 | 僅在真正需要橫切關注點(如日誌、驗證、DI)時使用,且保持單一職責 |
最佳實踐
- 保持裝飾器簡潔:只負責「註冊」或「包裝」行為,具體邏輯盡量抽到獨立函式或服務。
- 使用
reflect-metadata取得型別資訊:在需要依賴注入(DI)或驗證時,可自動取得參數型別。 - 明確命名:如
LogMethod、Validate、CacheResult,讓閱讀者一眼看出目的。 - 單元測試:裝飾器本身是函式,易於測試;測試應涵蓋「是否正確修改 descriptor」與「副作用是否符合預期」。
- 避免在生產環境保留
console.log:可使用環境變數控制是否啟用日誌裝飾器。
實際應用場景
| 場景 | 裝飾器的角色 | 範例 |
|---|---|---|
| API 路由自動註冊 | @Controller、@Get、@Post 等,將類別與方法映射成路由表 |
NestJS、routing-controllers |
| 日誌與效能統計 | @LogMethod、@MeasureTime 包裹方法,統一記錄執行時間與參數 |
企業級服務的監控系統 |
| 驗證與授權 | @Validate、@Roles 於參數或方法上,拋出錯誤或阻止存取 |
表單驗證、RBAC 授權 |
| 依賴注入(DI) | @Injectable、@Inject 於類別或建構子參數上,自動注入實例 |
Angular、NestJS |
| 快取機制 | @CacheResult 包裝純函式,根據參數緩存回傳值 |
高頻率讀取的資料查詢服務 |
| 事件綁定 | @Autobind 確保事件處理函式不會因 this 丟失而失效 |
前端 UI 元件(React、Vue) |
小技巧:在自建框架時,可先定義 元資料鍵(metadata key)如
'routes'、'required:param',統一管理,避免名稱衝突。
總結
- 裝飾器是 TypeScript 提供的元程式設計工具,能在類別、屬性、方法、存取子與參數上插入自訂行為。
- 只要在
tsconfig.json開啟experimentalDecorators(以及可選的emitDecoratorMetadata),即可使用。 - 透過 函式 的形式實作裝飾器,我們可以完成 日誌、驗證、DI、路由映射、快取 等常見需求,讓程式碼更具表意且易於維護。
- 使用時要注意 執行順序、返回值、以及
this綁定 的細節,並遵循 單一職責、保持簡潔 的原則,才能避免過度抽象帶來的可讀性問題。
掌握了裝飾器的基本語法與實務應用後,你將能在大型專案中更自如地抽離共通功能,提升開發效率與程式品質。祝開發順利,玩得開心! 🎉