日常程式碼中的 TypeScript 泛型
你正在撰寫一個回傳陣列第一個元素的函式。你希望它能適用於數字、字串和物件,但同時也希望回傳型別能符合輸入型別。如果不使用泛型,你只能求助於 any,並失去所有型別安全。這就是 TypeScript 泛型派上用場的地方。
泛型讓你能撰寫適用於多種型別的程式碼,同時保留具體的型別資訊。在本指南中,我們將探討在日常 TypeScript 程式碼中使用泛型的實用模式。
什麼是泛型?
泛型是一種將型別參數化的方式。就像函式接受參數一樣,泛型接受型別參數。它們允許你建立可重複使用的元件,適用於各種型別,而不是單一型別。
考慮這個 identity 函式:
function identity<T>(arg: T): T {
return arg;
}
在這裡,T 是一個型別變數。當你呼叫 identity(42) 時,TypeScript 會推斷 T 為 number。當你呼叫 identity("hello") 時,T 會變成 string。回傳型別總是符合輸入型別。
實務中的泛型函式
讓我們來看一個常見情境:一個將值包裝在物件中的函式。
function wrapInObject<T>(value: T): { value: T } {
return { value };
}
如果不使用泛型,你必須使用 any 或 unknown,而呼叫端會失去型別資訊。使用泛型時,回傳型別精確地是 { value: T }。
另一個日常例子是回傳陣列最後一個元素的函式:
function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
TypeScript 會從陣列參數推斷 T,所以 last([1, 2, 3]) 會回傳 number | undefined。
泛型約束
有時你想限制可與泛型一起使用的型別。例如,你可能需要存取只存在於特定型別上的屬性。你可以使用 extends 關鍵字來新增約束。
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
現在 T 必須有一個型別為 number 的 length 屬性。這適用於字串、陣列,以及任何具有 length 屬性的物件。
你也可以將約束與介面一起使用:
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
這確保傳遞給 findById 的項目具有 id 屬性。
泛型介面與型別別名
泛型不僅限於函式。你可以定義泛型介面和型別別名來建立靈活的資料結構。
interface ApiResponse<T> {
data: T;
status: number;
error?: string;
}
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
const data = await response.json();
return { data, status: response.status };
}
現在你可以精確地為 API 回應加上型別:
interface User {
id: number;
name: string;
}
const userResponse = await fetchData<User>('/api/user');
// userResponse.data is User
泛型類別
類別也可以是泛型的。一個常見例子是簡單的堆疊:
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
const num = numberStack.pop(); // number | undefined
React 元件中的泛型
如果你正在建置 React 元件,泛型可以讓你的元件更具可重複使用性。例如,一個渲染任何型別項目的列表元件:
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map((item, index) => <li key={index}>{renderItem(item)}</li>)}</ul>;
}
現在你可以將 List 與任何型別一起使用,並在 renderItem 回呼上獲得完整的型別檢查。
常見陷阱與最佳實踐
- 不要過度使用泛型。 如果一個函式只適用於特定型別,就直接使用該型別。泛型會增加複雜度。
- 使用描述性的型別參數名稱。 雖然
T很常見,但對於多個參數,請使用像TInput、TOutput這樣的名稱。 - 優先使用約束而非
any。 約束為你提供型別安全,同時仍然靈活。 - 記住泛型在執行時會被抹除。 它們只存在於編譯時期。
比較:泛型 vs. any vs. 聯合型別
| 方法 | 型別安全 | 靈活性 | 使用情境 |
|---|---|---|---|
any |
無 | 高 | 遷移 JS、快速原型 |
| 聯合型別 | 良好 | 限於已知型別 | 當你知道所有可能的型別時 |
| 泛型 | 強 | 高 | 可重複使用且保留型別的元件 |
常見問題
何時應該使用泛型而不是 any?
每當你想在函式或元件中保留型別資訊時,就使用泛型。any 會停用型別檢查,而泛型會保留它。如果你發現自己為了讓某些功能運作而求助於 any,請考慮泛型是否能更安全地解決問題。
我可以使用多個型別參數嗎?
可以。你可以定義具有多個型別參數的函式,例如 function pair<K, V>(key: K, value: V): [K, V]。當有多個參數時,請使用描述性的名稱。
泛型在 .tsx 檔案中如何與箭頭函式搭配使用?
在 .tsx 檔案中,<T> 可能與 JSX 產生歧義。請使用尾隨逗號:const identity = <T,>(arg: T): T => arg; 或使用 extends unknown:<T extends unknown>。
泛型是一項強大的功能,可以讓你的 TypeScript 程式碼更具可重複使用性和型別安全。從簡單的泛型函式開始,然後逐漸將它們應用到介面和類別。如果你需要以型別安全的方式格式化或驗證 JSON 資料,請查看我們的 JSON Formatter 工具,快速檢查並美化你的 JSON。