本文 AI 產出,尚未審核

TypeScript 裝飾器 – Metadata Reflection API 完全指南


簡介

在大型前端或 Node.js 專案中,型別資訊的即時取得往往是實作驗證、依賴注入 (DI) 或自動化路由等功能的關鍵。
TypeScript 於 2016 年加入 experimentalDecoratorsemitDecoratorMetadata 兩個編譯選項,讓開發者可以在執行階段透過 metadata reflection API 讀取編譯時產生的型別資訊。

本文將從概念說明、實作範例、常見陷阱與最佳實踐,完整帶你掌握這套機制,並展示在真實專案中的實務應用。


核心概念

1. 為什麼需要 Metadata Reflection?

  • 型別在編譯階段會被刪除:純 JavaScript 執行環境不會保留 TypeScript 的型別資訊。
  • 裝飾器提供鉤子:裝飾器本身只是一個函式,若沒有額外的機制,無法知道被裝飾的屬性是 stringnumber 還是自訂類別。
  • Reflect.metadata:透過 reflect-metadata 套件與 emitDecoratorMetadata,編譯器會在產出的 JavaScript 中自動插入 Reflect.defineMetadata 呼叫,讓我們在執行階段讀取型別。

重點:要使用此功能,必須同時開啟 experimentalDecoratorsemitDecoratorMetadata,且在入口檔案最上方 import 'reflect-metadata';


2. 基礎設定

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2017",
    "module": "commonjs",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "strict": true
  }
}
// index.ts (或 main.ts)
import 'reflect-metadata';

3. 常見的裝飾器類型

裝飾器 位置 典型用途
ClassDecorator 類別 註冊 DI container、加入全域元資料
PropertyDecorator 屬性 取得屬性型別、設定驗證規則
MethodDecorator 方法 讀取參數與回傳型別、實作 AOP
ParameterDecorator 參數 取得參數型別、注入服務

程式碼範例

以下示範 5 個實用範例,從最簡單的屬性型別取得,到自訂元資料與繼承情境。

3.1 取得屬性型別

import 'reflect-metadata';

function LogType(target: any, propertyKey: string) {
  const type = Reflect.getMetadata('design:type', target, propertyKey);
  console.log(`屬性 ${propertyKey} 的型別是:`, type.name);
}

class User {
  @LogType
  id: number;

  @LogType
  name: string;
}

/* 執行結果
屬性 id 的型別是: Number
屬性 name 的型別是: String
*/

說明design:type 由編譯器自動注入,回傳的是建構子函式(NumberStringArray…)。


3.2 取得方法參數與回傳型別

function LogParams(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const paramTypes: any[] = Reflect.getMetadata('design:paramtypes', target, propertyKey);
  const returnType = Reflect.getMetadata('design:returntype', target, propertyKey);
  console.log(`方法 ${propertyKey} 的參數型別:`, paramTypes.map(t => t.name));
  console.log(`方法 ${propertyKey} 的回傳型別:`, returnType.name);
}

class MathService {
  @LogParams
  add(a: number, b: number): number {
    return a + b;
  }
}

/* 執行結果
方法 add 的參數型別: [ 'Number', 'Number' ]
方法 add 的回傳型別: Number
*/

3.3 自訂 Metadata

function Required(target: any, propertyKey: string) {
  // 使用自訂的 key 'required' 設定 meta
  Reflect.defineMetadata('required', true, target, propertyKey);
}

function Validate(target: any) {
  // 讀取所有屬性的 required meta
  const keys = Object.getOwnPropertyNames(target.prototype);
  for (const key of keys) {
    const required = Reflect.getMetadata('required', target.prototype, key);
    if (required) {
      console.log(`屬性 ${key} 必須被提供`);
    }
  }
}

@Validate
class CreateUserDto {
  @Required
  username: string;

  @Required
  password: string;

  age?: number;
}

/* 執行結果
屬性 username 必須被提供
屬性 password 必須被提供
*/

技巧:自訂 key 只要避免與內建 key (design:*) 衝突即可。


3.4 繼承與 Metadata

class Base {
  @LogType
  createdAt: Date;
}

class Derived extends Base {
  @LogType
  updatedAt: Date;
}

/* 執行結果
屬性 createdAt 的型別是: Date
屬性 updatedAt 的型別是: Date
*/

重點Reflect.getMetadata 會沿原型鏈搜尋,故子類別可直接取得父類別的 metadata。


3.5 結合 DI Container 的實作

// container.ts
import 'reflect-metadata';
type Constructor<T = any> = new (...args: any[]) => T;
const container = new Map<Constructor, any>();

export function Injectable<T extends Constructor>(target: T) {
  // 讀取建構子參數型別
  const paramTypes: Constructor[] = Reflect.getMetadata('design:paramtypes', target) || [];
  const deps = paramTypes.map(dep => container.get(dep));
  const instance = new target(...deps);
  container.set(target, instance);
}

export function Inject(target: any, propertyKey: string) {
  const type = Reflect.getMetadata('design:type', target, propertyKey);
  Object.defineProperty(target, propertyKey, {
    get: () => container.get(type),
    enumerable: true,
    configurable: true,
  });
}
// services.ts
@Injectable
class Logger {
  log(msg: string) { console.log('[Log]', msg); }
}

@Injectable
class UserService {
  constructor(private logger: Logger) {}

  getUser(id: number) {
    this.logger.log(`取得使用者 ${id}`);
    return { id, name: 'Alice' };
  }
}

// app.ts
import { UserService } from './services';
const userService = Reflect.getMetadata('design:paramtypes', UserService)[0];
console.log(userService.getUser(1));

說明Injectable 會在註冊時自動解析建構子參數的型別,並以 Singleton 方式存入容器;Inject 則提供屬性注入的簡易寫法。


常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記 import 'reflect-metadata' 編譯不會報錯,但 Reflect.getMetadata 會回傳 undefined 在入口檔案最上方確保一次 import,或在每個需要的模組自行引入。
未開啟 emitDecoratorMetadata 只能取得自訂 metadata,無法取得型別資訊。 確認 tsconfig.jsonemitDecoratorMetadata: true
使用 any 或泛型 反射只能得到實際的 建構子,對 any、泛型會回傳 Object 若需要更精確資訊,請在裝飾器內自行傳遞類別參數(如 @Injectable(Logger))。
循環依賴 DI 容器在解析時可能產生無限迴圈。 盡量使用 延遲注入ProviderFactory)或分割模組。
效能問題 大量使用 Reflect.getMetadata 會有微小的執行成本。 只在啟動階段或一次性操作時讀取,之後快取結果。

最佳實踐

  1. 一次性載入型別資訊:在應用啟動時走一次「掃描」流程,將所有 metadata 快取於內存。
  2. 統一使用自訂 key:避免與 design:* 衝突,建議使用 myapp:* 前綴。
  3. 保持裝飾器純粹:裝飾器本身不應執行業務邏輯,只負責「註冊」或「標記」;真正的行為交給框架或服務層。
  4. 型別安全:在 TypeScript 中使用泛型與 Constructor<T> 讓 DI 容器的 get 方法有正確的型別推斷。

實際應用場景

1. 表單驗證 (class-validator)

import 'reflect-metadata';
import { IsString, IsInt, validate } from 'class-validator';

class RegisterDto {
  @IsString()
  username: string;

  @IsString()
  password: string;

  @IsInt()
  age: number;
}

// 透過 metadata,class-validator 能自動知道每個屬性的驗證規則

2. REST API 路由自動註冊 (routing-controllers)

import 'reflect-metadata';
import { Controller, Get, Param } from 'routing-controllers';

@Controller('/users')
class UserController {
  @Get('/:id')
  getOne(@Param('id') id: number) {
    return { id, name: 'Bob' };
  }
}

routing-controllers 會利用 design:paramtypes 取得參數型別,進而自動轉型。

3. ORM 映射 (TypeORM)

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity()
class Post {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;
}

TypeORM 依賴 reflect-metadata 讀取屬性型別,以決定資料庫欄位類型。

4. 事件驅動系統

function Subscribe(event: string) {
  return (target: any, methodName: string) => {
    const listeners = Reflect.getMetadata('event:listeners', globalThis) || {};
    listeners[event] = listeners[event] || [];
    listeners[event].push(target[methodName].bind(target));
    Reflect.defineMetadata('event:listeners', listeners, globalThis);
  };
}

透過自訂 metadata,任意類別只要加上 @Subscribe('UserCreated') 即可自動註冊事件處理函式。


總結

Metadata Reflection API 為 TypeScript 裝飾器提供了 執行階段的型別資訊,讓開發者能以宣告式、元資料驅動的方式構建 驗證、DI、路由、ORM 等高階功能。掌握以下要點,即可在專案中安全且高效地運用:

  1. 必備設定experimentalDecorators + emitDecoratorMetadata + import 'reflect-metadata'
  2. 熟悉內建 keydesign:typedesign:paramtypesdesign:returntype
  3. 自訂 metadata:使用 Reflect.defineMetadata / Reflect.getMetadata,並遵守命名規範。
  4. 避免常見陷阱:忘記載入套件、循環依賴、過度使用 any
  5. 實務化:將 metadata 轉為快取、結合 DI 容器或框架,提升程式碼可讀性與維護性。

透過本文的概念說明與完整範例,你已具備在 TypeScript 生態系中,利用 metadata reflection 構建彈性、可擴充的程式架構所需的全部基礎。祝你在開發旅程中玩得開心、寫程式更順手!