深入理解 TypeScript Decorator 裝飾器
前言
我第一次在工作裡接觸到 Decorator,是團隊後端從 Koa 技術轉型到 Nest.js 的那段期間。剛開始使用 Nest.js 時,其實我完全不清楚 Decorator 的原理,大多都是文件怎麼寫,我就怎麼寫——這裡放 @Controller、那裡加 @Injectable,照著文件寫能跑起來再說。但這些東西在 TypeScript 裡究竟是怎麼運作?背後為什麼要這樣設計?我其實完全沒有概念,一直停留在「照著範例抄」的階段。
最近想花點時間把 Decorator 的底層原理釐清,所以整理了這份筆記。內容包含我如何理解 Decorator、為什麼會需要它、以及我自己在實作過程中觀察到的行為,希望能把這些想法整理成一篇清楚的文章。
什麼是 Decorator?
Decorator 的本質
我們可以把 Decorator 想成是:「在不改原本程式邏輯的前提下,替類別或方法貼上一些標籤(metadata),後續的程式碼或框架可以根據這些標籤做事」。這樣的方式可以把業務邏輯跟額外的關注點(像是日誌、權限、快取)分開來看,程式更好維護也比較乾淨。
為什麼需要 Decorator?
在沒有 Decorator 之前,開發者們常常需要在每個方法內重複撰寫記錄日誌、權限檢查這類程式碼:
// 沒有 Decorator 的世界
class UserService {
createUser(name: string) {
// 手動加上日誌
console.log("開始創建用戶:", name);
// 手動檢查權限
if (!this.hasPermission()) {
throw new Error("沒有權限");
}
// 實際的業務邏輯
return { name, id: Math.random() };
}
updateUser(id: number, name: string) {
// 又要重複寫一次日誌
console.log("開始更新用戶:", id, name);
// 又要重複檢查權限
if (!this.hasPermission()) {
throw new Error("沒有權限");
}
// 實際的業務邏輯
return { id, name };
}
}
造成的結果是:
- 🔴 重複程式碼太多:每個方法都要寫日誌、檢查權限
- 🔴 業務邏輯不清晰:真正的邏輯被這些「雜事」淹沒了
- 🔴 難以維護:如果要改日誌格式,每個方法都要改
在有了 Decorator 之後,我們可以把程式改寫如下:
// 有 Decorator 的世界
class UserService {
@Log
@CheckPermission
createUser(name: string) {
// 只專注在業務邏輯
return { name, id: Math.random() };
}
@Log
@CheckPermission
updateUser(id: number, name: string) {
// 只專注在業務邏輯
return { id, name };
}
}
有 Decorator 輔助的好處顯而易見:
- ✅ 程式碼乾淨:業務邏輯一目了然
- ✅ 可重用:
@Log和@CheckPermission可以用在任何方法上 - ✅ 易維護:要改日誌格式只需要改一個地方
Decorator 的基礎語法
Decorator 使用 @expression 的形式,其中 expression 必須是一個函數:
@DecoratorName
class MyClass {}
@DecoratorName
method() {}
@DecoratorName
property: string;
如果需要傳入參數,通常會寫成「裝飾器工廠」(Decorator Factory):先呼叫一個函式傳參數,再回傳真正的裝飾器函式。
@DecoratorName("參數1", "參數2")
class MyClass {}
多個 Decorator 疊加的執行順序
一個方法可以有多個 Decorator,它們的執行順序是由下往上:
class Example {
@First
@Second
@Third
method() {}
}
// 執行順序:Third → Second → First
TypeScript Decorator 的五種類型
TypeScript 提供了五種 Decorator,分別可以裝飾不同的目標:
| Decorator 類型 | 裝飾目標 | 使用時機 |
|---|---|---|
Class Decorator | 類別 | 為類別添加元數據、修改建構函數 |
Method Decorator | 方法 | 日誌、性能測量、權限檢查 |
Property Decorator | 屬性 | 資料驗證、序列化配置 |
Accessor Decorator | getter/setter | 攔截屬性存取、快取 |
Parameter Decorator | 方法參數 | 參數驗證、依賴注入 |
接下來,這篇文章會一個一個深入介紹。
必要的 tsconfig.json 配置
由於 Decorator 是 TypeScript 的實驗性功能,預設是關閉的。我們必須在 tsconfig.json 中啟用它:
{
"compilerOptions": {
"experimentalDecorators": true, // 必須設為 true
"target": "ES2020", // 建議 ES2015 以上
"module": "commonjs"
}
}
由於 Decorator 目前還在 TC39(JavaScript 標準委員會)的提案階段。TypeScript 提前實現了這個功能,但語法可能會隨著標準演進而改變。不過也不用擔心,主流框架(Angular、NestJS、TypeORM)都在使用,並且已經相當穩定。
- 如果沒有啟用
experimentalDecorators,使用 Decorator 語法會報錯 - 對於 Parameter Decorator,還需要安裝
reflect-metadata套件(稍後會詳細說明)
Class Decorator
基本概念
Class Decorator 是應用在類別聲明上的函數。它接收類別的建構函數作為參數,可以觀察、修改或替換類別定義。
function ClassDecorator(constructor: Function) {
// constructor 就是被裝飾的類別
}
範例 1:觀察類別
最簡單的 Class Decorator,只是記錄類別的資訊:
function LogClass(constructor: Function) {
console.log("類別名稱:", constructor.name);
console.log("建構函數:", constructor);
}
@LogClass
class User {
name = "Bosh";
}
// 輸出:
// 類別名稱: User
// 建構函數: [class User]
- Decorator 是在類別定義時執行,不是在創建實例時
- 這意味著上面的
console.log會在程式編譯時就執行,而不是new User()時