同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~
(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)
你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?
你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?
就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。
一天只有24小时,时间永远不够用,常常感到力不从心。
技术行业,本就是逆水行舟,不进则退。
如果你也有同样的困扰,别慌。
从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲。
这一次,我们一起慢慢来,扎扎实实变强。
不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,
咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。
一、开篇:为什么要关心接口类型管理?
日常开发里经常会遇到:
- 全是 any:接口返回写
any,改数据结构时到处报错,只能靠手翻。 - 类型满天飞:
UserInfo、userInfo、User混用,不知道用哪一个。 - 和 axios 脱节:请求封装是封了,但响应类型还是要自己手动写。
- 团队风格不统一:有人写
.d.ts,有人写在组件里,维护成本高。
这些都和接口类型管理有关。不管理,就会有类型缺失、重复定义、请求/响应脱节等问题。
下面从概念 → 组织方式 → 实战规范 → 与 axios 结合 → 踩坑,把从 any 到规范的 api.d.ts 讲清楚。
二、概念扫盲:接口类型从哪里来?
2.1 什么是接口类型?
接口类型就是描述 API 请求参数和响应数据的 TS 类型,比如:
// 用户信息
interface UserInfo {
id: number;
name: string;
avatar?: string;
}
// 登录接口的请求参数
interface LoginParams {
username: string;
password: string;
}
// 登录接口的响应
interface LoginResponse {
token: string;
user: UserInfo;
}
有了这些类型,IDE 才能补全、检查错误,重构时也更安全。
2.2 常见三种写法对比
| 写法 | 优点 | 缺点 | 适用 |
|---|---|---|---|
直接写 any | 写得快 | 无类型检查、易出错 | 不推荐 |
| 类型写在组件/请求文件里 | 就近使用 | 难复用、难维护 | 简单小项目 |
统一放在 api.d.ts 或 types/ | 易复用、易维护、易和 axios 结合 | 需要前期规划 | 推荐 |
一句话:能统一放的就统一放,能分模块的就分模块。
三、从 any 到有组织的类型
3.1 典型 any 写法
// 登录
const login = (params: any) => {
return axios.post('/api/login', params);
};
// 使用
login({ username: 'admin', password: '123' }).then(res => {
console.log(res.data.user.name); // 没有提示,拼错也不知道
});
问题:参数、返回值都没有约束,重构、改接口时容易漏改。
3.2 改进思路:请求参数 + 响应数据类型化
// 先定义类型
interface LoginParams {
username: string;
password: string;
}
interface LoginResponse {
token: string;
user: {
id: number;
name: string;
avatar?: string;
};
}
// 再写请求
const login = (params: LoginParams): Promise<AxiosResponse<LoginResponse>> => {
return axios.post('/api/login', params);
};
// 使用时
login({ username: 'admin', password: '123' }).then(res => {
const user = res.data.user;
console.log(user.name); // 有类型提示
});
核心:为每个接口定义 Params 和 Response,并在请求函数上显式声明返回类型。
四、按模块拆分:api.d.ts 的组织方式
4.1 推荐目录结构
src/
├── types/
│ ├── api.d.ts # 汇总导出(可选)
│ ├── user.d.ts # 用户模块
│ ├── order.d.ts # 订单模块
│ ├── product.d.ts # 商品模块
│ └── common.d.ts # 公共类型
4.2 模块拆分原则
- 按业务模块分文件:user、order、product 等。
- 公共类型单独放:分页、通用枚举、基础结构。
- 命名规范统一:
XxxParams/XxxRequest,XxxResponse/XxxResult。
4.3 common.d.ts:公共类型
// common.d.ts
/** 通用分页参数 */
export interface PageParams {
page: number;
pageSize: number;
}
/** 通用分页响应 */
export interface PageResult<T> {
list: T[];
total: number;
}
/** 通用接口响应外壳(很多后端会包一层) */
export interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
说明:ApiResponse<T> 和 PageResult<T> 用来描述统一的数据结构,减少重复定义。
4.4 user.d.ts:用户模块
// user.d.ts
import type { ApiResponse } from './common';
/** 用户信息 */
export interface UserInfo {
id: number;
name: string;
avatar?: string;
role: string;
}
/** 登录请求参数 */
export interface LoginParams {
username: string;
password: string;
}
/** 登录响应 */
export interface LoginResponse {
token: string;
user: UserInfo;
}
/** 获取用户列表请求参数 */
export interface UserListParams {
keyword?: string;
page: number;
pageSize: number;
}
说明:一个业务模块的所有请求/响应类型放在一起,方便查找和修改。
4.5 api.d.ts:汇总导出(可选)
// api.d.ts
export * from './common';
export * from './user';
export * from './order';
export * from './product';
说明:如果项目不大,也可以直接从各模块 import;统一从 api.d.ts 导出更适合大型项目。
五、与 axios 封装结合
5.1 封装一个带类型的 request
很多项目会封装 request,并在请求和响应拦截器里统一处理 token、错误等。关键是让 request 支持泛型,这样每个接口都能拿到正确的响应类型。
// request.ts
import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios';
// 带泛型的 request
export function request<T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return axios.request(config);
}
// 使用示例
import type { LoginParams, LoginResponse } from '@/types/user';
export const login = (params: LoginParams) => {
return request<LoginResponse>({
url: '/api/login',
method: 'post',
data: params,
});
};
// 调用
login({ username: 'admin', password: '123' }).then(res => {
// res.data 自动推断为 LoginResponse
const token = res.data.token;
const user = res.data.user;
});
说明:request<T> 的泛型 T 表示 res.data 的类型,每个接口只需在调用 request 时传入对应响应类型。
5.2 处理统一响应外壳
如果后端统一包了一层 { code, message, data },可以单独定义一个包装类型:
// request.ts
import type { ApiResponse } from '@/types/common';
export function request<T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<ApiResponse<T>>> {
return axios.request(config);
}
// 登录接口
export const login = (params: LoginParams) => {
return request<LoginResponse>({
url: '/api/login',
method: 'post',
data: params,
});
};
// 使用时,res.data 是 ApiResponse<LoginResponse>
login({ username: 'admin', password: '123' }).then(res => {
if (res.data.code === 0) {
const { token, user } = res.data.data; // data 才是 LoginResponse
}
});
说明:request 的泛型是「业务 data」的类型,ApiResponse<T> 用来表示外层结构。
5.3 封装 get/post 简写
// request.ts
export const get = <T>(url: string, params?: object) => {
return request<T>({ url, method: 'get', params });
};
export const post = <T>(url: string, data?: object) => {
return request<T>({ url, method: 'post', data });
};
// 使用
import { get, post } from '@/utils/request';
import type { LoginResponse, UserListParams } from '@/types/user';
export const loginApi = (params: LoginParams) =>
post<LoginResponse>('/api/login', params);
export const getUserList = (params: UserListParams) =>
get<PageResult<UserInfo>>('/api/user/list', params);
说明:get、post 加上泛型,就能在调用时明确每个接口的响应类型。
六、实战:完整的请求文件示例
把类型定义和接口函数放在一起管理,一个模块一个文件,便于维护。
// api/user.ts
import { post, get } from '@/utils/request';
import type { LoginParams, LoginResponse, UserInfo, UserListParams } from '@/types/user';
import type { PageResult } from '@/types/common';
/** 登录 */
export const login = (params: LoginParams) => {
return post<LoginResponse>('/api/login', params);
};
/** 获取用户列表 */
export const getUserList = (params: UserListParams) => {
return get<PageResult<UserInfo>>('/api/user/list', params);
};
/** 获取用户详情 */
export const getUserDetail = (id: number) => {
return get<UserInfo>(`/api/user/${id}`);
};
说明:
- 类型从
types/user、types/common导入,不在此处重复定义。 - 每个接口都在
post/get上显式指定响应类型,保证调用处有完整类型提示。
七、选型与规范速查
| 场景 | 推荐做法 | 说明 |
|---|---|---|
| 小项目、接口少 | 在一个 api.d.ts 里集中写 | 简单够用 |
| 中大型项目 | 按模块拆分 user.d.ts、order.d.ts 等 | 易维护、易查找 |
| 请求封装 | request<T> 泛型 + 类型声明 | 和类型体系打通 |
| 命名 | XxxParams、XxxResponse | 统一风格 |
| 公共结构 | ApiResponse<T>、PageResult<T> | 减少重复 |
八、踩坑指南
| 坑 | 原因 | 建议 |
|---|---|---|
| 改了后端字段,前端不报错 | 类型没更新或用了 any | 定期对齐接口文档,更新 *.d.ts |
| 多个相似类型混淆 | User、UserInfo、user 混用 | 统一命名,必要时建 common.d.ts 共享 |
| 响应类型和实际不一致 | 没在请求函数上声明泛型 | 每个接口都写上 request<XxxResponse> |
| 分页、列表类型重复定义 | 每个接口都手写 { list, total } | 用 PageResult<T> 泛型 |
| 可选字段漏写 | 没加 ?,导致类型过严 | 按接口文档区分必选/可选 |
一个小技巧:用接口文档生成类型
如果后端有 Swagger/OpenAPI,可以用工具生成 api.d.ts,再按模块拆分和微调,减少手写和维护成本。
九、小结
| 层次 | 做法 | 典型场景 |
|---|---|---|
| 从 any 起步 | 为每个接口写 Params + Response | 所有项目 |
| 有组织 | 按模块拆分 *.d.ts | 中大型项目 |
| 和 axios 结合 | request<T> + 统一 ApiResponse | 封装请求层 |
记住三点:
- 不再用 any:至少为请求参数和响应数据定义类型。
- 按模块拆分:user、order、common 等,命名和结构统一。
- 和 axios 打通:用泛型
request<T>,在接口层显式声明响应类型。
把接口类型管理好,能减少很多隐蔽的 bug,重构和协作也会更轻松。
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~