日常程式碼中的 TypeScript 泛型:實用指南
你正在撰寫一個可處理任何型別的函式,但 TypeScript 一直抱怨 any。或者你為了處理不同型別,把同一個函式複製了三遍。泛型可以解決這個問題——它們讓你撰寫可處理多種型別的程式碼,同時保持型別安全。但許多開發者不是避開它們,就是過度使用。本指南將展示如何在日常程式碼中使用泛型,而不會頭痛。
什麼是泛型,為什麼你該關心?
泛型是一種參數化型別的方式。與其硬編碼像 string 或 number 這樣的型別,你可以使用一個型別變數(通常是 T),在函式或類別被使用時,它會被替換為具體的型別。
這為什麼重要?因為它讓你撰寫可重複使用且保持型別安全的程式碼。沒有泛型,你要麼複製程式碼,要麼使用 any,這就違背了 TypeScript 的初衷。
泛型函式:建構基礎
讓我們從一個簡單的例子開始。假設你想要一個回傳陣列第一個元素的函式。
function firstElement(arr: any[]): any {
return arr[0];
}
這可行,但你失去了型別資訊。如果你傳入一個字串陣列,回傳型別是 any,而不是 string。現在使用泛型:
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
現在 TypeScript 知道如果你傳入 string[],你會得到 string | undefined。型別被保留了。
你也可以使用多個型別參數:
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
泛型介面和型別別名
泛型不僅限於函式。你可以建立泛型介面和型別別名來描述靈活的資料結構。
interface Box<T> {
value: T;
}
const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };
這對 API 回應特別有用。與其為每個端點定義一個新介面,你可以定義一個泛型 ApiResponse<T>:
interface ApiResponse<T> {
data: T;
status: number;
}
type User = { id: number; name: string };
const response: ApiResponse<User> = {
data: { id: 1, name: "Alice" },
status: 200,
};
泛型約束:加入規則
有時你想限制泛型可以使用的型別。這就是約束的用途。使用 extends 關鍵字要求型別具有某些屬性。
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength("hello"); // OK
getLength([1, 2, 3]); // OK
getLength(42); // Error: number doesn't have length
約束讓你的泛型更可預測,並防止執行階段錯誤。
泛型類別
類別也可以是泛型的。這對堆疊、佇列或儲存庫等資料結構很方便。
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(1);
numberStack.push(2);
const top = numberStack.pop(); // number | undefined
何時使用泛型(以及何時不該使用)
泛型很強大,但如果過度使用,會讓程式碼更難閱讀。這裡有一個快速指南:
| 使用泛型的時機 | 避免泛型的時機 |
|---|---|
| 你需要保留型別關係 | 簡單的聯合型別或 any 就足夠 |
| 你正在撰寫可重複使用的工具 | 型別總是相同 |
| 你想避免程式碼重複 | 它讓程式碼更難理解 |
一個好的經驗法則:如果你發現自己為多種型別撰寫相同的邏輯,考慮使用泛型。如果你正在加入第三個型別參數,而且沒有人能理解,那就退一步。
常見模式與最佳實踐
- 為型別參數使用描述性名稱。
T對簡單情況沒問題,但TData或TResponse可能更清楚。 - 偏好約束而非
any。 如果你需要靈活性,使用extends來強制最小形狀。 - 利用預設型別參數。 你可以提供預設值:
<T = string>。 - 將泛型與工具型別一起使用。 TypeScript 內建的
Partial<T>、Readonly<T>和Record<K, V>都是泛型。
真實世界範例:泛型 API 客戶端
讓我們用一個實際例子來總結:一個簡單的 API 客戶端,它獲取資料並回傳具型別的回應。
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json() as Promise<T>;
}
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
async function main() {
const post = await fetchData<Post>("https://jsonplaceholder.typicode.com/posts/1");
console.log(post.title); // string
}
這種模式在前端程式碼中很常見。你獲得型別安全,而不需要為每個端點重複自己。
常見問題
TypeScript 中的 <T> 語法是什麼意思?
<T> 宣告一個型別參數。它是一個佔位符,代表將在函式、介面或類別被使用時指定的型別。你可以隨意命名,但 T 是單一型別的慣例。
我可以使用多個泛型型別參數嗎?
可以。你可以定義任意多個,用逗號分隔:<T, U, V>。使用描述性名稱來保持程式碼可讀性。
如何將泛型約束為只接受物件?
使用 T extends object。這確保 T 是物件型別,而不是像 string 或 number 這樣的原始型別。你也可以約束為特定形狀,例如 T extends { id: number }。
泛型是撰寫可維護 TypeScript 的核心部分。如果你正在處理 JSON 資料,並想快速驗證或格式化,試試我們的 JSON Formatter 來美化列印和檢查你的 API 回應。