本文 AI 產出,尚未審核

TypeScript 泛型類別(Generic Class)

簡介

在大型前端或後端專案中,型別安全 是維護程式品質的關鍵。TypeScript 透過靜態型別檢查,讓開發者在編譯階段就能捕捉到許多潛在的錯誤。而 泛型(Generics) 更是提升型別彈性與可重用性的強大工具。

本單元聚焦於 泛型類別,即把類別本身抽象化,使其能在不失去型別資訊的情況下,處理多種資料型別。掌握泛型類別不僅能讓程式碼更具表現力,還能減少重複實作,提升維護效率,對初學者與中階開發者皆相當重要。

核心概念

什麼是泛型類別?

泛型類別是一種在宣告時不指定具體型別,而在使用時再提供型別參數的類別。語法上,只需要在類別名稱後加上 <T>(或多個型別參數)即可:

class Box<T> {
  private _value: T;

  constructor(value: T) {
    this._value = value;
  }

  get value(): T {
    return this._value;
  }

  set value(newValue: T) {
    this._value = newValue;
  }
}

Box<T> 可以在建立實例時決定 TnumberstringUser 等任意型別,型別資訊會被完整保留,因此在編譯期就能獲得自動完成與錯誤提示。


為什麼要使用泛型類別?

場景 使用非泛型類別的問題 使用泛型類別的好處
資料容器 必須使用 anyObject,失去型別安全 保留真實型別,避免 runtime 錯誤
資料結構 重複寫 Stack<number>Stack<string> 等類別 單一實作即可支援多種型別
API 客戶端 每個 endpoint 需要不同的回傳型別 透過泛型參數自動推斷回傳型別

基本語法與使用方式

1. 單一型別參數

class Pair<K, V> {
  constructor(public key: K, public value: V) {}
}

// 使用
const numberPair = new Pair<number, string>(1, "one");
const stringPair = new Pair<string, boolean>("isReady", true);

2. 預設型別參數

class Cache<T = any> {
  private store = new Map<string, T>();

  set(key: string, value: T) {
    this.store.set(key, value);
  }

  get(key: string): T | undefined {
    return this.store.get(key);
  }
}

// 若不提供型別參數,預設為 any
const anyCache = new Cache();
anyCache.set("foo", 123); // OK

// 提供具體型別
const numberCache = new Cache<number>();
numberCache.set("pi", 3.14);

3. 限制型別參數(extends

interface Lengthwise {
  length: number;
}

class Collection<T extends Lengthwise> {
  private items: T[] = [];

  add(item: T) {
    console.log(`Item length: ${item.length}`);
    this.items.push(item);
  }
}

// 合法
const strCollection = new Collection<string>();
strCollection.add("hello");

// 錯誤:number 沒有 length 屬性
// const numCollection = new Collection<number>();

4. 多型別參數與交叉型別

type Constructor<T> = new (...args: any[]) => T;

class ServiceFactory<T, C extends Constructor<T>> {
  constructor(private Ctor: C) {}

  create(...args: ConstructorParameters<C>): T {
    return new this.Ctor(...args);
  }
}

// 範例:建立 User 服務
class User {
  constructor(public name: string, public age: number) {}
}

const userFactory = new ServiceFactory(User);
const user = userFactory.create("Alice", 30);

5. 靜態屬性與泛型

靜態屬性無法直接使用類別的型別參數,但可以透過 泛型函式 來間接操作:

class Mapper<T> {
  private map = new Map<string, T>();

  set(key: string, value: T) {
    this.map.set(key, value);
  }

  get(key: string): T | undefined {
    return this.map.get(key);
  }

  // 靜態方法:回傳對應型別的 Mapper 實例
  static create<U>(): Mapper<U> {
    return new Mapper<U>();
  }
}

// 使用靜態泛型方法
const numberMapper = Mapper.create<number>();
numberMapper.set("one", 1);

程式碼範例

範例 1:簡易資料緩存 (Cache)

class SimpleCache<K, V> {
  private storage = new Map<K, V>();

  /** 取得快取值,若不存在則回傳 undefined */
  get(key: K): V | undefined {
    return this.storage.get(key);
  }

  /** 設定快取值 */
  set(key: K, value: V): void {
    this.storage.set(key, value);
  }

  /** 清除指定 key */
  delete(key: K): boolean {
    return this.storage.delete(key);
  }
}

// 使用
const userCache = new SimpleCache<number, { name: string; age: number }>();
userCache.set(1, { name: "Bob", age: 28 });
const bob = userCache.get(1); // 型別為 { name: string; age: number } | undefined

範例 2:事件系統 (EventEmitter)

type Listener<T> = (payload: T) => void;

class EventEmitter<Events extends Record<string, any>> {
  private listeners: { [K in keyof Events]?: Listener<Events[K]>[] } = {};

  /** 訂閱事件 */
  on<K extends keyof Events>(eventName: K, listener: Listener<Events[K]>) {
    if (!this.listeners[eventName]) this.listeners[eventName] = [];
    this.listeners[eventName]!.push(listener);
  }

  /** 觸發事件 */
  emit<K extends keyof Events>(eventName: K, payload: Events[K]) {
    this.listeners[eventName]?.forEach((fn) => fn(payload));
  }
}

// 定義事件型別
interface AppEvents {
  login: { userId: string };
  logout: void;
}

// 建立實例
const appBus = new EventEmitter<AppEvents>();

appBus.on("login", (data) => {
  console.log(`User ${data.userId} logged in`);
});

appBus.emit("login", { userId: "A123" });

範例 3:資料驗證器 (Validator)

type Rule<T> = (value: T) => boolean | string;

class Validator<T> {
  private rules: Rule<T>[] = [];

  /** 加入驗證規則 */
  addRule(rule: Rule<T>) {
    this.rules.push(rule);
    return this; // 允許鏈式呼叫
  }

  /** 執行驗證,回傳錯誤訊息陣列(若無錯誤則為空) */
  validate(value: T): string[] {
    const errors: string[] = [];
    for (const rule of this.rules) {
      const result = rule(value);
      if (result !== true) errors.push(String(result));
    }
    return errors;
  }
}

// 使用 - 驗證字串長度與是否為 email
const emailValidator = new Validator<string>()
  .addRule((v) => (v.length >= 5 ? true : "長度不足"))
  .addRule((v) => (/^\S+@\S+\.\S+$/.test(v) ? true : "不是有效的 Email"));

const errors = emailValidator.validate("a@b.c");
console.log(errors); // ["長度不足"]

範例 4:樹結構 (TreeNode)

class TreeNode<T> {
  children: TreeNode<T>[] = [];

  constructor(public value: T) {}

  /** 新增子節點 */
  addChild(child: TreeNode<T>) {
    this.children.push(child);
  }

  /** 深度優先遍歷 */
  traverse(callback: (node: TreeNode<T>) => void) {
    callback(this);
    this.children.forEach((c) => c.traverse(callback));
  }
}

// 建立一棵字串樹
const root = new TreeNode<string>("root");
const childA = new TreeNode<string>("A");
const childB = new TreeNode<string>("B");
root.addChild(childA);
root.addChild(childB);
childA.addChild(new TreeNode<string>("A1"));

root.traverse((node) => console.log(node.value));

範例 5:型別安全的工廠模式 (Factory)

interface Product {
  price: number;
  description(): string;
}

class Book implements Product {
  constructor(public price: number, public title: string) {}
  description() {
    return `書名:《${this.title}》, 價格 $${this.price}`;
  }
}

class Pen implements Product {
  constructor(public price: number, public color: string) {}
  description() {
    return `筆的顏色:${this.color}, 價格 $${this.price}`;
  }
}

class ProductFactory<T extends Product> {
  create(...args: ConstructorParameters<new (...args: any[]) => T>): T {
    // @ts-ignore: TypeScript 無法直接推斷構造子型別,這裡用 any 暫時繞過
    return new (this.constructor as any)(...args);
  }
}

// 使用方式
const bookFactory = new ProductFactory<Book>();
const myBook = bookFactory.create(120, "TypeScript 入門");
console.log(myBook.description());

const penFactory = new ProductFactory<Pen>();
const myPen = penFactory.create(25, "藍色");
console.log(myPen.description());

常見陷阱與最佳實踐

陷阱 說明 解決方式
使用 any 逃避泛型 any 塞進泛型參數會失去型別安全,等同於不使用泛型。 盡量使用具體型別或 unknown,並在需要時透過型別斷言(type assertion)縮小範圍。
靜態屬性無法直接存取型別參數 靜態屬性在編譯時不會受到實例型別參數的影響。 使用 泛型工廠函式靜態泛型方法(如 static create<T>())來產生相對應的實例。
過度複雜的型別參數 多層嵌套的泛型會讓錯誤訊息變得難以閱讀。 盡量把複雜型別抽離成 type aliasinterface,保持類別宣告簡潔。
未限制型別參數導致不合理的使用 若未加 extends,使用者可以傳入任何型別,可能造成 runtime 錯誤。 為需要的屬性或方法加上 型別限制(如 T extends { id: number })。
忘記在子類別中傳遞型別參數 繼承泛型類別時,子類別若未寫 <T> 會導致型別變成 any 在子類別宣告時保持相同的泛型參數,例如 class Sub<T> extends Base<T> {}

最佳實踐

  1. 預設型別參數:給予常用的預設值,減少呼叫端的冗長。
  2. 型別限制:盡可能使用 extends 來描述型別需求,提升 IDE 提示品質。
  3. 保持單一職責:泛型類別應聚焦於單一概念,若需要多種行為,考慮拆分成多個類別或介面。
  4. 利用型別推斷:在大多數情況下,TypeScript 能自動推斷泛型參數,盡量省略顯式寫法,讓程式碼更簡潔。
  5. 測試覆蓋:即使有型別檢查,仍建議為關鍵的泛型類別寫單元測試,確保在不同型別下的行為一致。

實際應用場景

場景 為何適合使用泛型類別
前端狀態管理(如 Redux、MobX) Store、Action、State 都可以抽象為泛型類別,讓不同模組共享同一套框架。
資料庫抽象層(ORM) Repository<T> 能根據實體類別自動產生 CRUD 方法,減少重複程式碼。
API 客戶端 SDK HttpClient<TResponse> 讓每個 endpoint 的回傳型別在編譯期即確定,提升開發效率。
表單驗證框架 FormValidator<T> 依據表單模型自動產生對應的驗證規則與錯誤訊息。
遊戲開發的資料容器 Component<T>System<T> 等 ECS(Entity‑Component‑System)架構常用泛型類別來管理不同類型的元件。

範例:在一個簡易的 Redux‑like 狀態管理庫中,我們可以這樣寫:

type Reducer<S, A> = (state: S, action: A) => S;

class Store<S, A> {
  private state: S;
  private reducer: Reducer<S, A>;
  private listeners: (() => void)[] = [];

  constructor(initialState: S, reducer: Reducer<S, A>) {
    this.state = initialState;
    this.reducer = reducer;
  }

  dispatch(action: A) {
    this.state = this.reducer(this.state, action);
    this.listeners.forEach((l) => l());
  }

  getState(): S {
    return this.state;
  }

  subscribe(listener: () => void) {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter((l) => l !== listener);
    };
  }
}

// 使用
interface CounterState {
  count: number;
}
type CounterAction = { type: "inc" } | { type: "dec" };

const counterReducer: Reducer<CounterState, CounterAction> = (state, action) => {
  switch (action.type) {
    case "inc":
      return { count: state.count + 1 };
    case "dec":
      return { count: state.count - 1 };
    default:
      return state;
  }
};

const counterStore = new Store<CounterState, CounterAction>({ count: 0 }, counterReducer);
counterStore.subscribe(() => console.log(counterStore.getState()));
counterStore.dispatch({ type: "inc" }); // { count: 1 }

這樣一個 泛型類別 就能在不同的 State/Action 組合下重複使用,且不會失去型別安全。


總結

泛型類別是 TypeScript 提供的高階抽象工具,讓開發者能在 保持型別安全 的同時,寫出 可重用、可擴充 的程式碼。透過本篇的概念說明、實作範例與最佳實踐,你應該已經能:

  1. 宣告與使用 單一或多個型別參數的類別。
  2. 利用 extends、預設參數 等技巧限制型別,提升開發體驗。
  3. 辨識常見陷阱,避免因過度寬鬆的型別而產生錯誤。
  4. 在實務專案(如狀態管理、資料庫抽象、API 客戶端)中,將泛型類別作為核心設計模式。

掌握泛型類別後,你的 TypeScript 程式碼將更具彈性與可維護性,也能更好地配合大型團隊開發與長期迭代。祝你寫程式愉快,持續在型別安全的道路上前行! 🚀