TypeScript(TS)是一種靜態型別的超集合語言,旨在增強 JavaScript 的可維護性和可讀性。在 TS 中,泛型(Generics) 是一個強大而靈活的特性,允許我們在編寫函數、類別或接口時,不必預先定義型別,而是將型別作為參數來使用,進而實現更加靈活和可重用的代碼結構。
什麼是泛型?
泛型可以理解為「型別變數」,它允許開發者在編寫代碼時不指定具體型別,而是在使用時再決定具體型別。這使得代碼能適應更多場景,而不失去型別檢查的優勢。
泛型的基本語法
泛型使用尖括號 <T> 來表示,其中 T 是型別參數的名稱,可以是任何有效的標識符。
1 2 3
| function identity<T>(value: T): T { return value; }
|
使用泛型函數
在上述範例中,我們定義了一個泛型函數 identity,它接收任意型別的值並返回相同型別的值。使用時可以顯式地指定型別,或讓編譯器自動推斷。
1 2 3 4 5
| const result1 = identity<string>("Hello, TypeScript");
const result2 = identity(123);
|
泛型的應用場景
1. 泛型與陣列
我們可以用泛型來確保陣列中所有元素具有相同型別。
1 2 3 4 5 6
| function getFirstElement<T>(arr: T[]): T { return arr[0]; }
const firstNumber = getFirstElement([1, 2, 3]); const firstString = getFirstElement(["a", "b", "c"]);
|
2. 泛型與類別
泛型可以用於類別來定義靈活的資料結構。例如,一個通用的堆疊類別。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Stack<T> { private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); } }
const numberStack = new Stack<number>(); numberStack.push(10); console.log(numberStack.pop());
|
3. 泛型與接口
泛型可以幫助接口適應多種型別場景。
1 2 3 4 5 6 7 8 9
| interface KeyValuePair<K, V> { key: K; value: V; }
const pair: KeyValuePair<string, number> = { key: "age", value: 30, };
|
4. 泛型約束
有時候我們希望泛型不只是「任意型別」,而是需要滿足某些條件。這可以通過 extends 關鍵字來實現。
1 2 3 4 5 6
| function logProperty<T extends { name: string }>(obj: T): void { console.log(obj.name); }
logProperty({ name: "Alice", age: 25 });
|
泛型 vs any
很多開發者可能會想:「為什麼要使用泛型?直接使用 any 不是更簡單嗎?」
泛型與 any 都能處理多種型別,但它們有本質上的區別:
any 的特性
- 靈活但無約束:
any 允許任何型別的值,但在編譯階段不進行型別檢查。
- 可能導致型別安全問題:
any 使得編譯器無法提供錯誤提示,容易引發運行時錯誤。
範例:
1 2 3 4 5 6
| function processValue(value: any): any { return value; }
const result = processValue(123); result.toUpperCase();
|
泛型的特性
- 靈活且安全:泛型在保證靈活性的同時,仍然保留了型別檢查。
- 提升代碼可讀性:泛型讓函數或類別的型別關係更加明確。
範例:
1 2 3 4 5 6
| function processValue<T>(value: T): T { return value; }
const result = processValue(123);
|
比較表
| 特性 |
any |
泛型 (T) |
| 靈活性 |
高 |
高 |
| 型別安全性 |
無 |
有 |
| 編譯階段檢查 |
無 |
有 |
| 可讀性 |
可能模糊 |
清晰,與業務邏輯緊密相關 |
| 適用場景 |
型別未知的臨時解決方案 |
可重用的泛型結構 |
何時使用泛型而不是 any
- 當你需要在函數內部使用參數的特定方法或屬性時
- 當你需要確保函數的返回值類型與輸入參數類型相關時
- 當你需要在編譯時捕獲可能的型別錯誤時
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function firstElement<T>(arr: T[]): T | undefined { return arr[0]; }
function firstElementAny(arr: any[]): any { return arr[0]; }
const numbers = [1, 2, 3]; const firstNumber = firstElement(numbers); const strings = ["a", "b", "c"]; const firstString = firstElement(strings);
const firstAny = firstElementAny(numbers); const len: number = firstAny.length;
|
實際應用場景
1. API 響應處理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| interface ApiResponse<T> { data: T; status: number; message: string; }
async function fetchData<T>(url: string): Promise<ApiResponse<T>> { const response = await fetch(url); return response.json(); }
interface User { id: number; name: string; }
const user = await fetchData<User>('/api/user/1');
|
2. 狀態管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class State<T> { private value: T;
constructor(initialValue: T) { this.value = initialValue; }
getValue(): T { return this.value; }
setValue(newValue: T): void { this.value = newValue; } }
const numberState = new State<number>(0); const stringState = new State<string>("");
|
最佳實踐
明確的類型參數名稱:使用有意義的泛型參數名稱,例如在處理集合時使用 T,在處理鍵值對時使用 K 和 V。
適度使用泛型約束:使用 extends 關鍵字來限制泛型類型,確保類型安全。
避免過度使用:只在真正需要靈活性的地方使用泛型,過度使用會增加程式碼的複雜度。
結論
泛型是一個強大的特性,它能幫助我們寫出更靈活、更可重用的程式碼,同時保持類型安全。
記住,泛型的主要目的是幫助我們寫出更通用的程式碼,同時保持類型安全。在實際開發中,合理使用泛型可以大大提高程式碼的可維護性和重用性。