TypeScript 泛型完整教學:前端工程師必學的型別參數化技巧
如果你寫 TypeScript 寫了一陣子,一定會在某個時刻遇到泛型(Generics)。第一次看到那個角括號 <T> 的時候,老實說我也是一臉問號。但當我真正搞懂之後,才發現泛型根本就是 TypeScript 裡最強大的武器之一。今天這篇文章,我會用最白話的方式帶你從零開始學會 TypeScript 泛型,讓你在日常開發中真正用得上。
什麼是 TypeScript 泛型?為什麼需要它?
簡單來說,泛型就是「型別的參數化」。就像函式可以接收參數一樣,泛型讓你的型別也能接收參數。這樣做的好處是什麼?你可以寫出既有彈性、又保有型別安全的程式碼。
舉個最常見的例子,假設你要寫一個函式,把傳進來的值原封不動地回傳:
// 不用泛型的寫法 — 失去型別資訊
function identity(value: any): any {
return value;
}
const result = identity("hello"); // result 的型別是 any,完全沒用
用了 any 之後,TypeScript 的型別檢查形同虛設。但如果改用泛型:
// 泛型寫法 — 保留完整型別資訊
function identity<T>(value: T): T {
return value;
}
const result = identity("hello"); // result 的型別是 string ✓
const num = identity(42); // num 的型別是 number ✓
看到了嗎?<T> 就像一個「型別變數」,在你呼叫函式的時候,TypeScript 會自動推斷 T 是什麼型別。這就是泛型的核心概念——寫一次,到處適用,而且完全不犧牲型別安全。
泛型基礎語法:你的第一個泛型函式
泛型的語法其實沒那麼難。慣例上我們會用大寫字母 T 代表 Type,但你也可以用任何名稱,像 U、K、V 這些都很常見。多個型別參數用逗號隔開:
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const p = pair("name", 25); // 型別是 [string, number]
你也可以在呼叫時手動指定型別,雖然多數情況下 TypeScript 的型別推斷已經夠聰明了:
const p2 = pair<string, boolean>("active", true);
我個人的建議是:能讓 TypeScript 自動推斷就讓它推斷,除非推斷結果不符合你的預期時再手動指定。這樣程式碼比較乾淨。
泛型函式的進階用法
泛型真正好用的地方,在於處理陣列和複雜資料結構。來看幾個實用的範例:
// 取得陣列的第一個元素
function getFirst<T>(arr: T[]): T | undefined {
return arr[0];
}
const firstNum = getFirst([10, 20, 30]); // number | undefined
const firstStr = getFirst(["a", "b", "c"]); // string | undefined
// 合併兩個物件
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
const merged = merge({ name: "靜宜" }, { age: 28 });
// 型別是 { name: string } & { age: number }
注意到上面 merge 函式裡的 extends object 了嗎?這就是泛型約束,等等會更深入介紹。
泛型介面與泛型型別別名
泛型不只能用在函式上,介面(interface)和型別別名(type alias)也可以用泛型。這在前端開發中超級實用,尤其是處理 API 回傳資料的時候:
// 泛型介面:API 回應的通用結構
interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: string;
}
// 使用時指定具體的資料型別
interface User {
id: string;
name: string;
email: string;
}
type UserResponse = ApiResponse<User>;
type UserListResponse = ApiResponse<User[]>;
// 搭配 fetch 使用
async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
const res = await fetch(url);
return res.json();
}
const users = await fetchApi<User[]>("/api/users");
// users.data 的型別是 User[] ✓
這種模式我在實際專案裡用得非常多。一個 ApiResponse<T> 介面就能統一管理所有 API 的回應格式,既省力又安全。如果你正在用 Next.js Server Actions 或任何後端框架,這個模式都非常適合搭配使用。
泛型約束:用 extends 限縮型別範圍
有時候你不希望泛型接受所有型別,而是要限定範圍。這時候就要用到 extends 關鍵字:
// 限制 T 必須有 length 屬性
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength("hello"); // ✓ string 有 length
getLength([1, 2, 3]); // ✓ array 有 length
getLength(123); // ✗ 編譯錯誤!number 沒有 length
另一個超級實用的模式是 keyof 搭配泛型:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "靜宜", age: 28, role: "engineer" };
const name = getProperty(user, "name"); // 型別是 string
const age = getProperty(user, "age"); // 型別是 number
getProperty(user, "email"); // ✗ 編譯錯誤!"email" 不是有效的 key
這招我真的超愛用。它讓你存取物件屬性的時候,不僅 key 是安全的,連回傳值的型別都會正確推斷。寫起來安心多了。
內建 Utility Types 與泛型的關係
TypeScript 內建了很多 Utility Types,它們本身就是用泛型實作的。理解這些工具型別,對實際開發幫助非常大:
interface Todo {
title: string;
description: string;
completed: boolean;
createdAt: Date;
}
// Partial — 所有屬性變為可選
type EditTodo = Partial<Todo>;
// Pick — 只選取部分屬性
type TodoPreview = Pick<Todo, "title" | "completed">;
// Omit — 排除特定屬性
type TodoWithoutDate = Omit<Todo, "createdAt">;
// Record — 建立鍵值對型別
type TodoStatus = Record<"pending" | "done" | "archived", Todo[]>;
// Required — 所有屬性變為必填
type StrictTodo = Required<Partial<Todo>>;
這些 Utility Types 在跟 React 19 搭配使用時特別好用,例如處理元件的 props 型別時,Partial 和 Pick 可以省下大量重複定義的工作。
泛型在 React 中的實戰應用
身為前端工程師,泛型在 React 開發中的應用場景非常多。我來分享幾個我自己在專案中常用的模式:
// 泛型元件:可複用的列表元件
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// 使用時自動推斷型別
<List
items={users}
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => user.id}
/>
另一個超實用的場景是泛型 custom hook:
// 泛型 Hook:通用的 API 請求 Hook
function useApi<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then((json: T) => setData(json))
.catch(err => setError(err))
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// 使用時指定回傳資料型別
const { data: users } = useApi<User[]>("/api/users");
// users 的型別是 User[] | null ✓
如果你正在用 React Server Components,泛型也能幫助你定義伺服器端和客戶端元件之間的資料傳遞型別,確保整條資料流都是型別安全的。而在 Next.js App Router 架構下,泛型更是管理複雜路由資料的好幫手。
結語:泛型是前端工程師的超能力
學完這篇之後,你應該已經掌握了 TypeScript 泛型的核心概念——從基礎語法、泛型函式、泛型介面,到泛型約束和 Utility Types,再到 React 中的實戰應用。
我自己的經驗是,剛開始學泛型的時候會覺得有點抽象,但只要在實際專案中多寫幾次,很快就會變成直覺。特別是當你開始封裝可複用的元件和 Hook 時,泛型幾乎是不可或缺的。
最後給你一個小建議:不要為了用泛型而用泛型。如果一個函式只處理特定型別,就直接寫死型別就好,沒必要硬搞泛型。泛型的價值在於「真的需要彈性」的時候,讓你的程式碼既靈活又安全。
希望這篇教學對你有幫助。如果你覺得這篇文章不錯,歡迎分享給也在學 TypeScript 的朋友!
你可能也喜歡
探索其他領域的精選好文