TypeScript 如何做泛型请求封装
写过前端请求的人,大概率都踩过“重复造轮子”的坑:每次加新接口,都要对着 axios 写一遍请求参数、返回值的类型声明,改个字段名得翻遍所有接口文件。要是团队里新人接手,还得对着“哪里该改类型”“为什么要加泛型”摸不着头脑。其实用 TypeScript 的泛型封装请求,既能省重复工作量,又能把接口类型锁死,写代码时还能自动补全提示。
先搞懂:泛型到底帮我们解决了什么?
泛型简单说就是“给代码装个可换的模板”。比如你要做一杯果汁,模板是“水果→榨汁→果汁”,水果可以是苹果、橙子、草莓——换水果不用重新写榨汁流程,只要告诉模板“这次用草莓就行”。

放到请求封装里,这个模板就是“发送请求→拿到返回值→处理数据”,每次接口的返回值类型不一样,用泛型就能让同一个封装函数,适配所有不同的接口,还不会丢类型提示。举个生活化例子:你开了个跑腿服务,模板是“接单→取货→送件”,单是奶茶、文件、快递,都能用这个流程,只要每次告诉模板“这次要送奶茶”就行。
怎么用泛型封装基础请求?
先拿最常用的 axios 举例,写一个通用的请求函数。首先,axios 本身的请求和返回类型太泛,我们加个泛型变量 T,用来“占位”接口的返回值类型。
比如,封装一个 request 函数:
`typescript
import axios from 'axios';
// T 是泛型,代表接口返回数据的类型
async function request<T>(url: string, params?: Record<string, any>) {
const response = await axios.get(url, { params });
// 把返回的 data 转换成 T 类型,这样用的时候就有类型提示了
return response.data as T;
}
`
这里的 T 就是泛型,等真正调用的时候再给它赋值。比如调用“获取用户信息”的接口,它的返回值是 { id: number; name: string; age: number },写调用代码的时候就把 T 换成这个类型:
`typescript
// 定义接口的返回类型
interface UserInfo {
id: number;
name: string;
age: number;
}
// 调用封装好的请求,T 就是 UserInfo,返回值自动是 UserInfo 类型
const getUser = async (userId: number) => {
const user = await request<UserInfo>('/api/user', { id: userId });
// 这时候 user.name 会有类型提示,点一下就能出来,不会写错成 userName
console.log(user.name);
};
`
要是用 POST 请求,逻辑也一样,只要把方法改成 post,参数换成请求体就行:
`typescript
async function postRequest<T>(url: string, data?: Record<string, any>) {
const response = await axios.post(url, data);
return response.data as T;
}
`
加个请求通用返回结构的适配
大部分后端接口都会套个统一的格式,比如不管成功失败,返回都是 { code: number; msg: string; data: T },data 才是我们要的业务数据。这时候只要把泛型套到这个结构里就行,不用写额外的判断。
改一下封装函数,让它适配这个通用格式:
`typescript
interface ApiResponse<T> {
code: number;
msg: string;
data: T;
}
async function request<T>(url: string, params?: Record<string, any>) {
const response = await axios.get(url, { params });
// 这里的 ApiResponse<T> 就是告诉 TypeScript,返回的结构是带泛型的通用格式
const apiRes = response.data as ApiResponse<T>;
// 可以加个简单的错误处理,比如 code 不是 200 就抛错
if (apiRes.code !== 200) {
throw new Error(apiRes.msg || '请求失败');
}
return apiRes.data;
}
`
现在调用的时候就更顺了,还是拿用户信息举例,不用管外层的 code 和 msg,直接拿到 data 里的内容:
`typescript
const getUser = async (userId: number) => {
// 泛型 UserInfo 会自动套到 ApiResponse 里,最终返回的是 UserInfo 类型
const user = await request<UserInfo>('/api/user', { id: userId });
// user 直接是 { id: 1, name: "张三", age: 25 },类型完全锁死
};
`
实际用的时候要注意的小细节
封装不是写死的,要留灵活度。比如有的接口参数是固定的,有的是可选的,泛型可以和其他类型配合用。要是有文件上传这种特殊请求,单独写个 uploadRequest 就行,不用把所有情况揉在一个函数里。
还有,别滥用泛型。要是一个接口的返回类型很简单,比如只是返回 { success: boolean },直接写死类型比用泛型更省心,不用硬加模板。核心是把“重复的请求逻辑”和“变化的接口类型”分开,泛型就是把变化的部分用模板代替,既省时间又保类型安全。