目標讀者:剛學 TS 的前端開發者,或希望把泛型用到實際工程(請求封裝、組件復用)中的同學。
目錄
- 為什么需要泛型(直觀動機)
- 基本語法與例子(函數、接口、類)
- 泛型約束(
extends
、keyof
) - 進階語法:默認類型、多個類型參數、泛型推斷
- 實戰一:
request<T>
網絡請求封裝(詳細講解) - 實戰二:React 通用下拉組件
<Select<T>>
(含使用示例) - 常見坑、調試技巧與最佳實踐
- 練習題與參考資料
1. 為什么需要泛型(直觀動機)
在沒有泛型的世界里,如果你寫一個工具函數或組件只能處理單一類型,就會出現大量重復代碼或喪失類型提示。
舉例:寫一個返回第一個元素的 first
函數,如果不使用泛型,你可能寫成 any
,失去類型安全:
function firstBad(arr: any[]) { return arr[0]; }
const a = firstBad([1,2,3]); // a 的類型是 any,編輯器不會提示
使用泛型后:
function first<T>(arr: T[]): T | undefined { return arr[0]; }
const a = first([1,2,3]); // a 被推斷為 number | undefined
泛型能讓工具/組件“對所有類型通用”,同時保留類型信息,這就是它的價值。
2. 基本語法與例子
2.1 泛型函數
// 最基礎的泛型函數:identity
function identity<T>(arg: T): T {return arg;
}const s = identity('hello'); // T 被推斷為 string
const n = identity<number>(123); // 顯示指定泛型
注意:一般情況下不必顯式寫 <T>
,TypeScript 會根據參數自動推斷。
2.2 泛型類型別名 / 接口
type Box<T> = { value: T };
const b: Box<number> = { value: 42 };interface ApiResponse<T> {code: number;data: T;
}const r: ApiResponse<string[]> = { code: 0, data: ['a','b'] };
2.3 泛型類
class Stack<T> {private items: T[] = [];push(item: T) { this.items.push(item); }pop(): T | undefined { return this.items.pop(); }
}const s = new Stack<number>();
s.push(1);
2.4 多個類型參數
function mapArray<T, U>(arr: T[], fn: (t: T) => U): U[] {return arr.map(fn);
}const r = mapArray([1,2,3], x => x.toString()); // r: string[]
3. 泛型約束(extends
、keyof
)
有時候我們要限制泛型的“范圍”,比如只允許對象類型、必須包含某些屬性等。
3.1 extends
限制
function pluck<T extends object, K extends keyof T>(obj: T, key: K) {return obj[key];
}const user = { id: '1', name: 'Alice' };
pluck(user, 'name'); // OK
// pluck(user, 'notExist'); // Error
解釋:K extends keyof T
表示 K
必須是 T
的鍵之一,防止傳入不存在的屬性名。
3.2 keyof
的常見用法
type KeysOfUser = keyof typeof user; // 'id' | 'name'
4. 進階語法(默認類型、泛型推斷等)
4.1 默認類型
function identityDefault<T = string>(arg: T): T { return arg; }
const a = identityDefault('x'); // T 推斷為 string
4.2 泛型推斷
TypeScript 會根據函數參數自動推斷泛型類型,像 identity([1,2,3])
會推斷 T
為 number[]
的元素類型(… 具體依賴簽名)。
5. 實戰一:封裝 request<T>
(網絡請求)
目的:寫一個簡單且實用的 request
,在調用處能用泛型指定返回類型,從而獲得完整的類型提示。
5.1 需求與設計
- 希望
request<T>(url)
返回Promise<T>
。 - 在大多數場景后端返回的是一個包裹結構,比如
{ code: number, data: T }
,我們也要支持。 - 稍微封裝錯誤處理與超時(示例化,不追求復雜性)。
5.2 代碼實現(utils/request.ts
)
// utils/request.ts
export type ApiResponse<T> = { code: number; data: T; message?: string };export async function request<T = any>(url: string, init?: RequestInit): Promise<T> {const controller = new AbortController();const timeout = setTimeout(() => controller.abort(), 10_000);try {const res = await fetch(url, { signal: controller.signal, ...init });if (!res.ok) throw new Error(res.statusText);const data = await res.json();return data as T; // 注意:這是類型斷言,運行時不會做檢查} finally {clearTimeout(timeout);}
}
5.3 使用示例
// types.ts
type User = { id: string; name: string };// 使用(直接返回數組)
const users = await request<User[]>('/api/users');
users[0].name; // 編輯器會提示 name// 使用(后端返回包裹結構)
const resp = await request<ApiResponse<User[]>>('/api/users-pkg');
const list = resp.data; // 正常使用
5.4 提醒:類型安全與運行時驗證
TypeScript 的類型只存在編譯階段。request<T>
中的 return data as T
是“信任后端返回的結構”。如果需要更嚴格的保證,請在運行時做校驗(使用 zod
、io-ts
等)。
6. 實戰二:React 通用下拉組件 <Select<T>>
(簡單到常用)
目標:實現一個對數據類型“透明”的下拉組件,使用泛型后,父組件拿到 onChange
的回調類型時能直接獲得具體類型提示。
6.1 需求與設計
- 組件接收
options: T[]
。 - 需要
getLabel?: (item: T) => string
,用于渲染文本。 - 需要
keyExtractor?: (item: T, idx: number) => string | number
,用于key
和value
(避免假設數據有id
字段)。 onChange?: (item: T | null) => void
。
6.2 組件代碼(簡潔、可用)
import React from 'react';export interface SelectProps<T> {options: T[];value?: T | null;onChange?: (item: T | null) => void;placeholder?: string;getLabel?: (item: T) => string;keyExtractor?: (item: T, idx: number) => string | number;
}// 注意箭頭函數組件寫法:const Select = <T,>(props: SelectProps<T>) => { ... }
export const Select = <T,>({ options, value, onChange, placeholder, getLabel, keyExtractor }: SelectProps<T>) => {const labelOf = getLabel ?? ((it: T) => String((it as any)));const keyOf = keyExtractor ?? ((_: T, idx: number) => idx);return (<selectvalue={options.indexOf(value as T)}onChange={(e) => {const idx = Number(e.target.value);onChange?.(idx >= 0 ? options[idx] : null);}}><option value={-1}>{placeholder ?? '請選擇'}</option>{options.map((it, i) => (<option key={String(keyOf(it, i))} value={i}>{labelOf(it)}</option>))}</select>);
};
說明:
const Select = <T,>(...)
中的,
(逗號)是一個常用寫法,用來避免 TSX 將<T>
誤解析為 JSX;這是聲明泛型函數表達式/箭頭函數時的語法技巧。- 為了讓組件與任意數據結構配合,我們沒有假定
item
有id
或label
字段,而是通過keyExtractor
與getLabel
注入策略。
6.3 使用示例
// App.tsx
import React, { useState, useEffect } from 'react';
import { Select } from './Select';
import { request } from './utils/request';type User = { id: string; name: string };function App() {const [users, setUsers] = useState<User[]>([]);const [sel, setSel] = useState<User | null>(null);useEffect(() => {request<User[]>('/api/users').then(setUsers).catch(console.error);}, []);return (<div><Selectoptions={users}value={sel}onChange={(u) => setSel(u)}getLabel={(u) => u.name}keyExtractor={(u) => u.id}placeholder="選擇用戶"/><div>當前選中:{sel ? sel.name : '無'}</div></div>);
}
類型體驗:當你寫 onChange={(u) => setSel(u)}
時,編輯器會推斷 u
的類型為 User | null
,這給你編輯器級別的保護與提示。
6.4 關于顯式泛型(什么時候必須)
通常只要 options
的類型是具體的數組(User[]
),TS 能推斷出 T
,使用時不需要寫 <Select<User> />
。
如果推斷失敗(例如 options
類型被擦除為 any[]
),你可以:
- 在數據源處把類型寫清楚(推薦);
- 或在組件使用處做類型斷言:
<Select options={someAny as User[]} ... />
。
7. 常見坑、調試技巧與最佳實踐
- 不要濫用
any
:泛型的一個目標就是替代any
,保留類型信息。 - 理解類型與運行時的邊界:泛型只是編譯期工具,運行時沒有類型檢查。
- 在庫/公共代碼中多寫泛型,在應用層用具體類型;庫需要更強的泛型設計能力。
- 避免過度復雜的類型:當類型系統變得難以理解時,權衡是否用運行時校驗來代替復雜類型。
- 在 React 中盡量依賴類型推斷,不要在 JSX 里頻繁顯式寫
<Component<Type> />
(有時會引起解析問題)。
8. 練習題(自測)
- 寫一個泛型
filterMap<T, U>
,它的簽名為(arr: T[], fn: (t: T) => U | null) => U[]
。 - 基于
request<T>
,寫一個getJson<T>(url)
,當后端返回{ code, data }
結構時,自動返回data
。 - 修改
Select
組件,使它支持multiple
(多選)并確保類型安全。
9. 總結與下一步學習建議
- 泛型讓你的代碼既通用又類型安全,是編寫可復用工具與組件的核心。
- 推薦掌握:泛型約束(
extends
)、keyof
、條件類型(下一步,可學infer
)、以及常見內置工具類型(Partial/Readonly/Record
)。