TypeScript 泛型入门(新手友好、完整详解) - 详解
2025-09-19 22:25 tlnshuju 阅读(63) 评论(0) 收藏 举报目标读者:刚学 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 {
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 = (props: SelectProps) => { ... }
export const Select = ({ options, value, onChange, placeholder, getLabel, keyExtractor }: SelectProps) => {
const labelOf = getLabel ?? ((it: T) => String((it as any)));
const keyOf = keyExtractor ?? ((_: T, idx: number) => idx);
return (
{
const idx = Number(e.target.value);
onChange?.(idx >= 0 ? options[idx] : null);
}}
>
{placeholder ?? '请选择'}
{options.map((it, i) => (
{labelOf(it)}
))}
);
};
说明:
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([]);
const [sel, setSel] = useState(null);
useEffect(() => {
request('/api/users').then(setUsers).catch(console.error);
}, []);
return (
setSel(u)}
getLabel={(u) => u.name}
keyExtractor={(u) => u.id}
placeholder="选择用户"
/>
当前选中:{sel ? sel.name : '无'}
);
}
类型体验:当你写 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)。
浙公网安备 33010602011771号