本文 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'];
  }
}

此模式常見於 NestJSrouting‑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)時使用,且保持單一職責

最佳實踐

  1. 保持裝飾器簡潔:只負責「註冊」或「包裝」行為,具體邏輯盡量抽到獨立函式或服務。
  2. 使用 reflect-metadata 取得型別資訊:在需要依賴注入(DI)或驗證時,可自動取得參數型別。
  3. 明確命名:如 LogMethodValidateCacheResult,讓閱讀者一眼看出目的。
  4. 單元測試:裝飾器本身是函式,易於測試;測試應涵蓋「是否正確修改 descriptor」與「副作用是否符合預期」。
  5. 避免在生產環境保留 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 綁定 的細節,並遵循 單一職責保持簡潔 的原則,才能避免過度抽象帶來的可讀性問題。

掌握了裝飾器的基本語法與實務應用後,你將能在大型專案中更自如地抽離共通功能,提升開發效率與程式品質。祝開發順利,玩得開心! 🎉