本文 AI 產出,尚未審核

TypeScript 類別泛型完整教學

簡介

在日常開發中,我們常會遇到需要 重複使用相同邏輯卻處理不同資料型別 的情境。傳統的 JavaScript 只能靠 any 或手動寫多個類別來因應,這樣不僅失去型別安全,也會造成程式碼冗長、維護成本高。
TypeScript 引入 類別泛型(Class Generics),讓我們可以在 同一個類別裡 以參數化的方式描述資料型別,從而同時享有 靜態型別檢查高度的可重用性

本篇文章將從概念說明、實作範例、常見陷阱到實務應用,完整掌握類別泛型的使用技巧,幫助 初學者到中階開發者 在專案中寫出更乾淨、更安全的程式碼。


核心概念

1. 為什麼需要類別泛型?

  • 型別安全:編譯階段即能捕捉錯誤,避免執行時的意外。
  • 程式碼復用:同一套邏輯可支援多種資料型別,避免重複撰寫。
  • 可讀性提升:使用者能清楚看到類別在何種型別上運作,文件化程度更高。

2. 基本語法

在 TypeScript 中,泛型使用 尖括號 <T> 表示。對於類別而言,泛型參數放在類別名稱之後:

class Box<T> {
  private _value: T;

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

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

  set value(v: T) {
    this._value = v;
  }
}
  • T 是一個 類型參數,在實例化時會被具體型別取代。
  • 任何屬性、方法的參數或回傳值,都可以使用 T 來保持一致性。

3. 多個泛型參數

有時候一個類別需要同時管理多種型別,這時可以宣告 多個泛型參數

class Pair<K, V> {
  constructor(public key: K, public value: V) {}
}
  • KV 代表鍵與值的型別,使用時自行指定。

4. 泛型約束(Constraints)

若希望泛型必須具備特定屬性或方法,可使用 extends限制型別

interface Lengthwise {
  length: number;
}

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

  add(item: T) {
    console.log(`新增長度為 ${item.length} 的項目`);
    this.items.push(item);
  }
}
  • 只有具備 length 屬性的型別(如 stringArray)才能作為 T 使用。

5. 預設泛型型別

有時候想提供 預設型別,讓使用者在不指定時仍能得到合理的行為:

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);
  }
}

程式碼範例

以下提供 5 個實用範例,說明類別泛型在不同情境下的應用與寫法。

範例 1️⃣:通用資料容器 Stack<T>

class Stack<T> {
  private items: T[] = [];

  /** 推入元素 */
  push(item: T): void {
    this.items.push(item);
  }

  /** 彈出元素,若空則回傳 undefined */
  pop(): T | undefined {
    return this.items.pop();
  }

  /** 目前的長度 */
  get size(): number {
    return this.items.length;
  }
}

// 使用
const numberStack = new Stack<number>();
numberStack.push(10);
numberStack.push(20);
console.log(numberStack.pop()); // 20

const stringStack = new Stack<string>();
stringStack.push('hello');
stringStack.push('world');
console.log(stringStack.pop()); // 'world'

重點Stack<T> 只在宣告一次後,就能支援任意型別的堆疊。


範例 2️⃣:鍵值對資料結構 Dictionary<K, V>

class Dictionary<K extends string | number, V> {
  private map = new Map<K, V>();

  set(key: K, value: V): void {
    this.map.set(key, value);
  }

  get(key: K): V | undefined {
    return this.map.get(key);
  }

  has(key: K): boolean {
    return this.map.has(key);
  }
}

// 使用
const userDict = new Dictionary<number, { name: string; age: number }>();
userDict.set(1, { name: 'Alice', age: 28 });
userDict.set(2, { name: 'Bob', age: 32 });

console.log(userDict.get(1)); // { name: 'Alice', age: 28 }

技巧K 限制為 string | number,確保 Map 的鍵能被正確比較。


範例 3️⃣:帶有約束的 SortableCollection<T>

interface Comparable {
  compareTo(other: this): number; // <0 表示小於, =0 表示相等, >0 表示大於
}

class SortableCollection<T extends Comparable> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  /** 依 compareTo 進行升序排列 */
  sort(): T[] {
    return this.items.sort((a, b) => a.compareTo(b));
  }
}

// 範例型別
class Person implements Comparable {
  constructor(public name: string, public age: number) {}

  compareTo(other: Person): number {
    return this.age - other.age; // 年齡小的排前面
  }
}

// 使用
const people = new SortableCollection<Person>();
people.add(new Person('Tom', 45));
people.add(new Person('Anna', 30));
people.add(new Person('Mike', 38));

console.log(people.sort().map(p => p.name)); // ['Anna', 'Mike', 'Tom']

關鍵:透過 extends Comparable,保證傳入的型別一定實作 compareTo 方法,使 sort 能安全運作。


範例 4️⃣:預設泛型的快取 Cache<T>

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

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

  /** 取得快取 */
  get(key: string): T | undefined {
    return this.storage.get(key);
  }

  /** 清除快取 */
  delete(key: string): boolean {
    return this.storage.delete(key);
  }
}

// 使用(未指定泛型,預設 any)
const anyCache = new Cache();
anyCache.set('foo', 123);
anyCache.set('bar', { x: 1 });
console.log(anyCache.get('bar')); // { x: 1 }

// 使用(指定泛型為 string)
const stringCache = new Cache<string>();
stringCache.set('greeting', 'Hello, world!');
console.log(stringCache.get('greeting')); // 'Hello, world!'

說明:若開發者不想指定型別,any 仍能保有彈性;若需要嚴格型別檢查,只要在宣告時提供具體型別即可。


範例 5️⃣:結合泛型與繼承的 EventEmitter<T>

type Listener<E> = (event: E) => void;

class EventEmitter<E> {
  private listeners: Listener<E>[] = [];

  /** 註冊監聽器 */
  on(listener: Listener<E>): void {
    this.listeners.push(listener);
  }

  /** 發送事件 */
  emit(event: E): void {
    for (const listener of this.listeners) {
      listener(event);
    }
  }
}

// 定義事件型別
interface UserEvent {
  type: 'login' | 'logout';
  userId: number;
}

// 使用
const userEmitter = new EventEmitter<UserEvent>();
userEmitter.on(e => console.log(`User ${e.userId} ${e.type}`));

userEmitter.emit({ type: 'login', userId: 7 }); // User 7 login
userEmitter.emit({ type: 'logout', userId: 7 }); // User 7 logout

實務意義:透過泛型,EventEmitter 能在不同專案中重複使用,只要提供相對應的事件介面,即可得到完整的型別提示與檢查。


常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記在實例化時提供型別 若類別有必須的泛型約束,未提供會導致 any,失去型別安全。 明確指定 <T>,或在類別內部提供預設型別。
過度使用 any 為了省事直接使用 any,會讓編譯器失去檢查能力。 儘量使用 具體型別或泛型約束,保持靜態檢查。
約束不夠嚴謹 使用 extends object 仍允許不具備所需屬性的型別。 介面或型別別名 明確列出必須的屬性或方法。
泛型與 this 類型混用 在類別方法返回 this 時,若未使用 this 型別,子類別的鏈式呼叫會失效。 使用 this 型別return this;)或 <U extends this> 進行泛型自參照。
遺忘 readonly 若資料不應被外部修改,忘記加 readonly 會造成意外變更。 為不可變屬性加上 readonly,或在介面中定義為只讀。

最佳實踐

  1. 盡量使用介面或型別別名 來描述泛型約束,讓意圖更清晰。
  2. 在公開 API 中提供預設泛型,降低使用門檻,同時保持彈性。
  3. 避免在同一檔案中混用多個相似的泛型類別,可考慮抽象成基礎類別或 mixin。
  4. 單元測試:對於泛型類別的行為(尤其是約束)寫測試,確保未來重構不會破壞型別安全。

實際應用場景

  1. 資料庫 ORM

    • 每個資料表都對應一個 模型類別,使用 <T>Repository<T> 能操作任意實體,且在編譯時即檢查欄位名稱與型別。
  2. 表單驗證框架

    • Form<T> 透過泛型描述表單欄位結構,getValue<K extends keyof T>(key: K) 能保證返回值型別正確,減少手動類型斷言。
  3. 狀態管理(Redux / Vuex)

    • Store<S> 中的 S 代表全局狀態型別,所有 getStatedispatch 都會根據 S 產生對應的型別提示,防止錯誤的 action payload。
  4. 通用 UI 元件

    • DataTable<T> 只需要一個欄位描述 columns: Array<keyof T>,即可自動產生類型安全的渲染邏輯,避免欄位寫錯或遺漏。
  5. 事件驅動系統

    • 如前面的 EventEmitter<E>,在大型前端或後端專案中,透過統一的事件介面,讓不同模組之間的傳遞資料保持一致且可被編譯器檢查。

總結

類別泛型是 TypeScript 提供的強大工具,讓我們能在 單一類別中 同時支援多種資料型別,同時保持 編譯時的型別安全。透過 泛型參數、約束、預設型別 等機制,我們可以:

  • 寫出可重用、可維護的通用類別(如 Stack、Dictionary、Cache)。
  • 在大型專案中保持 API 的一致性與可預測性(ORM、表單、狀態管理)。
  • 避免常見的型別錯誤與程式碼冗餘,提升開發效率。

只要遵守 最佳實踐(明確約束、避免過度使用 any、加上 readonly),配合適當的單元測試,類別泛型將成為你在 TypeScript 生態系統中不可或缺的武器。快把今天學到的概念套用到實際專案中,讓程式碼更安全、更具彈性吧! 🚀