本文 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> 可以在建立實例時決定 T 為 number、string、User 等任意型別,型別資訊會被完整保留,因此在編譯期就能獲得自動完成與錯誤提示。
為什麼要使用泛型類別?
| 場景 | 使用非泛型類別的問題 | 使用泛型類別的好處 |
|---|---|---|
| 資料容器 | 必須使用 any 或 Object,失去型別安全 |
保留真實型別,避免 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 alias 或 interface,保持類別宣告簡潔。 |
| 未限制型別參數導致不合理的使用 | 若未加 extends,使用者可以傳入任何型別,可能造成 runtime 錯誤。 |
為需要的屬性或方法加上 型別限制(如 T extends { id: number })。 |
| 忘記在子類別中傳遞型別參數 | 繼承泛型類別時,子類別若未寫 <T> 會導致型別變成 any。 |
在子類別宣告時保持相同的泛型參數,例如 class Sub<T> extends Base<T> {}。 |
最佳實踐
- 預設型別參數:給予常用的預設值,減少呼叫端的冗長。
- 型別限制:盡可能使用
extends來描述型別需求,提升 IDE 提示品質。 - 保持單一職責:泛型類別應聚焦於單一概念,若需要多種行為,考慮拆分成多個類別或介面。
- 利用型別推斷:在大多數情況下,TypeScript 能自動推斷泛型參數,盡量省略顯式寫法,讓程式碼更簡潔。
- 測試覆蓋:即使有型別檢查,仍建議為關鍵的泛型類別寫單元測試,確保在不同型別下的行為一致。
實際應用場景
| 場景 | 為何適合使用泛型類別 |
|---|---|
| 前端狀態管理(如 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 提供的高階抽象工具,讓開發者能在 保持型別安全 的同時,寫出 可重用、可擴充 的程式碼。透過本篇的概念說明、實作範例與最佳實踐,你應該已經能:
- 宣告與使用 單一或多個型別參數的類別。
- 利用
extends、預設參數 等技巧限制型別,提升開發體驗。 - 辨識常見陷阱,避免因過度寬鬆的型別而產生錯誤。
- 在實務專案(如狀態管理、資料庫抽象、API 客戶端)中,將泛型類別作為核心設計模式。
掌握泛型類別後,你的 TypeScript 程式碼將更具彈性與可維護性,也能更好地配合大型團隊開發與長期迭代。祝你寫程式愉快,持續在型別安全的道路上前行! 🚀