TypeScript – 泛型函式(Generic Functions)
簡介
在日常開發中,我們常會寫出需要接受不同型別參數的函式,例如 Array.prototype.map、Promise.then 等。若僅以 any 或聯合型別(union type)來描述,會失去 型別安全 與 自動完成 的好處,導致錯誤只能在執行時才被發現。
泛型函式(generic functions)正是為了解決這個問題:它讓函式在呼叫時根據傳入的實際型別自動推斷返回值的型別,從而在編譯階段就捕捉到潛在錯誤,提升程式的可讀性與維護性。
本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,完整介紹如何在 TypeScript 中撰寫與使用泛型函式,適合剛接觸 TypeScript 的初學者,也能為中階開發者提供進階技巧。
核心概念
1. 為什麼需要泛型函式?
- 型別推斷:函式可以根據實際傳入的參數自動決定返回值的型別。
- 避免
any:使用any會失去編譯期檢查,而泛型保持完整的型別資訊。 - 重複使用:相同的邏輯可以在不同型別上重複使用,減少程式碼冗餘。
2. 基本語法
function identity<T>(value: T): T {
return value;
}
<T>:在函式名稱後的尖括號中宣告一個類型參數T。value: T:參數的型別使用剛才宣告的類型參數。: T:返回值的型別同樣是T,表示返回值與傳入值型別相同。
呼叫方式
const num = identity<number>(42); // T 被推斷為 number
const str = identity('hello'); // T 自動推斷為 string
若在呼叫時省略類型參數,TypeScript 會根據實際參數自動推斷(稱為 type inference)。
3. 多個類型參數
有時候函式需要同時操作多個不同型別,此時可以宣告多個類型參數:
function merge<A, B>(obj1: A, obj2: B): A & B {
return { ...obj1, ...obj2 };
}
A & B為交叉型別,表示返回值同時擁有A與B的屬性。
4. 限制類型參數(Constraints)
若希望類型參數只能是某個型別的子型別,可以使用 extends 來加上限制:
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
此函式只接受具有 length 屬性的參數(如 string、Array、TypedArray 等),若傳入不符合的型別,編譯器會報錯。
5. 預設類型參數
在某些情況下,我們想為類型參數提供預設值:
function createArray<T = string>(length: number, value: T): T[] {
return Array.from({ length }, () => value);
}
若呼叫時未指定 T,則預設為 string。
程式碼範例
範例 1:基本的 identity 函式
function identity<T>(value: T): T {
// 直接回傳傳入的值,型別保持不變
return value;
}
// 使用
const num = identity(123); // num 的型別是 number
const bool = identity(true); // bool 的型別是 boolean
const arr = identity([1, 2, 3]); // arr 的型別是 number[]
範例 2:使用多個類型參數的 merge 函式
function merge<A, B>(obj1: A, obj2: B): A & B {
// 將兩個物件展開合併,返回交叉型別
return { ...obj1, ...obj2 };
}
// 使用
const person = { name: 'Alice', age: 30 };
const job = { title: 'Engineer', salary: 80000 };
const employee = merge(person, job);
// employee 的型別為 { name: string; age: number; } & { title: string; salary: number; }
// 可以直接存取所有屬性
console.log(employee.name, employee.title);
範例 3:帶限制的 getProperty 函式
取得物件指定屬性的值,同時保證屬名一定存在於物件型別中。
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// 使用
const user = { id: 1, username: 'bob', isAdmin: false };
const username = getProperty(user, 'username'); // username 的型別是 string
// const wrong = getProperty(user, 'password'); // 編譯錯誤:'password' 不在 keyof user
範例 4:預設類型參數的 createArray
function createArray<T = string>(length: number, value: T): T[] {
return Array.from({ length }, () => value);
}
// 使用
const nums = createArray<number>(3, 0); // [0, 0, 0],型別為 number[]
const strs = createArray(2, 'hi'); // ['hi', 'hi'],型別推斷為 string[]
範例 5:遞迴型別的 flatten(實務中常見的陣列扁平化)
type NestedArray<T> = (T | NestedArray<T>)[];
function flatten<T>(arr: NestedArray<T>): T[] {
const result: T[] = [];
for (const item of arr) {
if (Array.isArray(item)) {
result.push(...flatten(item)); // 再次呼叫自身,型別仍然是 T[]
} else {
result.push(item);
}
}
return result;
}
// 使用
const mixed = [1, [2, [3, 4]], 5];
const flat = flatten(mixed); // flat 的型別是 number[]
此範例展示了 泛型遞迴 的威力:不論陣列層級多深,最終都會得到正確的元素型別。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
忘記加上 extends 限制 |
若對類型參數未加限制,可能在函式內部使用不存在的屬性,導致編譯錯誤或 any 回退。 |
依需求使用 extends 限制,例如 T extends { length: number }。 |
過度使用 any |
為了避免型別錯誤而直接寫 any,失去泛型帶來的好處。 |
盡量讓 TypeScript 自動推斷,或使用 型別斷言(as)在確定安全時才使用。 |
| 類型參數命名不具意義 | 使用單一的 T、U、V 會讓程式碼難以閱讀。 |
為類型參數取具體名稱,如 Item, Key, Result,提升可讀性。 |
| 返回值型別寫錯 | 若返回值的型別寫成 any 或與參數不一致,會失去型別檢查。 |
確保返回型別正確映射到類型參數(如 T[]、A & B)。 |
| 遞迴型別深度過大 | 在遞迴泛型(如深層嵌套陣列)時,編譯器可能因型別過於複雜而報錯。 | 盡量限制遞迴深度或使用 unknown 作為中介型別,必要時拆分成多個函式。 |
最佳實踐
- 讓型別推斷自動化:盡量不在呼叫端顯式指定類型參數,除非推斷失敗或需要特別指定。
- 使用
keyof& 索引存取型別:在需要根據鍵名存取屬性時,配合K extends keyof T可保證鍵的正確性。 - 保持函式單一職責:泛型函式仍應遵守單一職責原則,過度複雜的泛型會降低可讀性。
- 寫測試:即使有型別保護,仍建議撰寫單元測試,確保邏輯正確。
- 工具輔助:使用 VS Code 的 IntelliSense、TypeScript Playground 來即時觀察型別推斷結果,快速驗證泛型寫法。
實際應用場景
資料轉換工具
在前端與後端交互時,常需要把 API 回傳的 JSON 轉換成特定介面的物件。利用泛型函式可以寫出一次性、型別安全的mapper:function mapDto<T, U>(dto: T, mapper: (src: T) => U): U { return mapper(dto); }通用資料結構(Stack、Queue、Tree)
實作資料結構時,元素型別往往不固定。使用泛型函式搭配類別,可一次支援所有型別:class Stack<T> { private items: T[] = []; push(item: T) { this.items.push(item); } pop(): T | undefined { return this.items.pop(); } }表單驗證函式庫
表單欄位的驗證規則會根據欄位型別不同而不同。透過泛型,我們可以寫出validate<T>(value: T, rule: (v: T) => boolean),保證傳入的驗證規則與值型別相符。React / Vue 組件的 Props 泛型
在 UI 框架中,組件常需要根據傳入的props產生對應的型別。例如:type ListProps<T> = { items: T[]; renderItem: (item: T) => React.ReactNode; }; function List<T>({ items, renderItem }: ListProps<T>) { return <ul>{items.map(renderItem)}</ul>; }函式式程式設計(Functional Programming)
pipe、compose等高階函式常使用泛型來保證前後函式的輸入輸出相容:function pipe<A, B, C>(fn1: (a: A) => B, fn2: (b: B) => C) { return (a: A) => fn2(fn1(a)); }
總結
泛型函式是 TypeScript 強大型別系統的核心之一,它讓我們在 保持型別安全 的同時,寫出 可重用、可讀性高 的程式碼。透過 T、extends、keyof 等語法,我們可以:
- 讓函式根據呼叫時的實際型別自動推斷返回值型別。
- 限制類型參數的範圍,避免非法操作。
- 在多型別、交叉型別、遞迴型別等複雜情境下仍保持型別正確性。
在實務開發中,從簡單的 identity、merge 到資料結構、表單驗證、React 組件,泛型函式都扮演著不可或缺的角色。只要遵循 正確的限制、清晰的命名 與 適度的抽象,就能在專案中最大化地發揮 TypeScript 的型別優勢,降低 bug 數量、提升開發效率。
祝你在 TypeScript 的旅程中,玩得開心、寫得順手! 🚀