目标读者:刚学 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
)。