TypeScript 裝飾器 – Metadata Reflection API 完全指南
簡介
在大型前端或 Node.js 專案中,型別資訊的即時取得往往是實作驗證、依賴注入 (DI) 或自動化路由等功能的關鍵。
TypeScript 於 2016 年加入 experimentalDecorators 與 emitDecoratorMetadata 兩個編譯選項,讓開發者可以在執行階段透過 metadata reflection API 讀取編譯時產生的型別資訊。
本文將從概念說明、實作範例、常見陷阱與最佳實踐,完整帶你掌握這套機制,並展示在真實專案中的實務應用。
核心概念
1. 為什麼需要 Metadata Reflection?
- 型別在編譯階段會被刪除:純 JavaScript 執行環境不會保留 TypeScript 的型別資訊。
- 裝飾器提供鉤子:裝飾器本身只是一個函式,若沒有額外的機制,無法知道被裝飾的屬性是
string、number還是自訂類別。 - Reflect.metadata:透過
reflect-metadata套件與emitDecoratorMetadata,編譯器會在產出的 JavaScript 中自動插入Reflect.defineMetadata呼叫,讓我們在執行階段讀取型別。
重點:要使用此功能,必須同時開啟
experimentalDecorators、emitDecoratorMetadata,且在入口檔案最上方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由編譯器自動注入,回傳的是建構子函式(Number、String、Array…)。
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.json 中 emitDecoratorMetadata: true。 |
使用 any 或泛型 |
反射只能得到實際的 建構子,對 any、泛型會回傳 Object。 |
若需要更精確資訊,請在裝飾器內自行傳遞類別參數(如 @Injectable(Logger))。 |
| 循環依賴 | DI 容器在解析時可能產生無限迴圈。 | 盡量使用 延遲注入(ProviderFactory)或分割模組。 |
| 效能問題 | 大量使用 Reflect.getMetadata 會有微小的執行成本。 |
只在啟動階段或一次性操作時讀取,之後快取結果。 |
最佳實踐
- 一次性載入型別資訊:在應用啟動時走一次「掃描」流程,將所有 metadata 快取於內存。
- 統一使用自訂 key:避免與
design:*衝突,建議使用myapp:*前綴。 - 保持裝飾器純粹:裝飾器本身不應執行業務邏輯,只負責「註冊」或「標記」;真正的行為交給框架或服務層。
- 型別安全:在 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 等高階功能。掌握以下要點,即可在專案中安全且高效地運用:
- 必備設定:
experimentalDecorators+emitDecoratorMetadata+import 'reflect-metadata'。 - 熟悉內建 key:
design:type、design:paramtypes、design:returntype。 - 自訂 metadata:使用
Reflect.defineMetadata/Reflect.getMetadata,並遵守命名規範。 - 避免常見陷阱:忘記載入套件、循環依賴、過度使用
any。 - 實務化:將 metadata 轉為快取、結合 DI 容器或框架,提升程式碼可讀性與維護性。
透過本文的概念說明與完整範例,你已具備在 TypeScript 生態系中,利用 metadata reflection 構建彈性、可擴充的程式架構所需的全部基礎。祝你在開發旅程中玩得開心、寫程式更順手!