接口类型管理:从 any 到有组织的 api.d.ts

同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~

(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)

你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?

你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?

就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。

一天只有24小时,时间永远不够用,常常感到力不从心。

技术行业,本就是逆水行舟,不进则退。

如果你也有同样的困扰,别慌。

从现在开始,跟着我一起心态归零 ,利用碎片时间,来一次彻彻底底的基础扫盲

这一次,我们一起慢慢来,扎扎实实变强。

不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,

咱们一起稳步积累,真正摆脱"面向搜索引擎写代码"的尴尬。

一、开篇:为什么要关心接口类型管理?

日常开发里经常会遇到:

  • 全是 any :接口返回写 any,改数据结构时到处报错,只能靠手翻。
  • 类型满天飞UserInfouserInfoUser 混用,不知道用哪一个。
  • 和 axios 脱节:请求封装是封了,但响应类型还是要自己手动写。
  • 团队风格不统一 :有人写 .d.ts,有人写在组件里,维护成本高。

这些都和接口类型管理有关。不管理,就会有类型缺失、重复定义、请求/响应脱节等问题。

下面从概念 → 组织方式 → 实战规范 → 与 axios 结合 → 踩坑 ,把从 any 到规范的 api.d.ts 讲清楚。

二、概念扫盲:接口类型从哪里来?

2.1 什么是接口类型?

接口类型就是描述 API 请求参数和响应数据的 TS 类型,比如:

typescript 复制代码
// 用户信息
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.tstypes/ 易复用、易维护、易和 axios 结合 需要前期规划 推荐

一句话:能统一放的就统一放,能分模块的就分模块。

三、从 any 到有组织的类型

3.1 典型 any 写法

typescript 复制代码
// 登录
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 改进思路:请求参数 + 响应数据类型化

typescript 复制代码
// 先定义类型
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 推荐目录结构

bash 复制代码
src/
├── types/
│   ├── api.d.ts          # 汇总导出(可选)
│   ├── user.d.ts         # 用户模块
│   ├── order.d.ts        # 订单模块
│   ├── product.d.ts      # 商品模块
│   └── common.d.ts       # 公共类型

4.2 模块拆分原则

  1. 按业务模块分文件:user、order、product 等。
  2. 公共类型单独放:分页、通用枚举、基础结构。
  3. 命名规范统一XxxParams / XxxRequestXxxResponse / XxxResult

4.3 common.d.ts:公共类型

typescript 复制代码
// 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:用户模块

typescript 复制代码
// 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:汇总导出(可选)

typescript 复制代码
// 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 支持泛型,这样每个接口都能拿到正确的响应类型。

typescript 复制代码
// 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 },可以单独定义一个包装类型:

typescript 复制代码
// 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 简写

typescript 复制代码
// 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);

说明:getpost 加上泛型,就能在调用时明确每个接口的响应类型。

六、实战:完整的请求文件示例

把类型定义和接口函数放在一起管理,一个模块一个文件,便于维护。

typescript 复制代码
// 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/usertypes/common 导入,不在此处重复定义。
  • 每个接口都在 post/get 上显式指定响应类型,保证调用处有完整类型提示。

七、选型与规范速查

场景 推荐做法 说明
小项目、接口少 在一个 api.d.ts 里集中写 简单够用
中大型项目 按模块拆分 user.d.tsorder.d.ts 易维护、易查找
请求封装 request<T> 泛型 + 类型声明 和类型体系打通
命名 XxxParamsXxxResponse 统一风格
公共结构 ApiResponse<T>PageResult<T> 减少重复

八、踩坑指南

原因 建议
改了后端字段,前端不报错 类型没更新或用了 any 定期对齐接口文档,更新 *.d.ts
多个相似类型混淆 UserUserInfouser 混用 统一命名,必要时建 common.d.ts 共享
响应类型和实际不一致 没在请求函数上声明泛型 每个接口都写上 request<XxxResponse>
分页、列表类型重复定义 每个接口都手写 { list, total } PageResult<T> 泛型
可选字段漏写 没加 ?,导致类型过严 按接口文档区分必选/可选

一个小技巧:用接口文档生成类型

如果后端有 Swagger/OpenAPI,可以用工具生成 api.d.ts,再按模块拆分和微调,减少手写和维护成本。

九、小结

层次 做法 典型场景
从 any 起步 为每个接口写 Params + Response 所有项目
有组织 按模块拆分 *.d.ts 中大型项目
和 axios 结合 request<T> + 统一 ApiResponse 封装请求层

记住三点:

  1. 不再用 any:至少为请求参数和响应数据定义类型。
  2. 按模块拆分:user、order、common 等,命名和结构统一。
  3. 和 axios 打通 :用泛型 request<T>,在接口层显式声明响应类型。

把接口类型管理好,能减少很多隐蔽的 bug,重构和协作也会更轻松。


学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。

后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。

关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。

如果你觉得这篇内容对你有帮助,不妨点赞收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。

我是 Eugene,你的电子学友,我们下一篇干货见~

相关推荐
喝咖啡的女孩1 小时前
React Hook & Class
前端
小呆呆_小乌龟1 小时前
同样是定义对象,为什么 TS 里有人用 interface,有人用 type?
前端·react.js
Forever7_1 小时前
仅用一个技巧,让 JavaScript 性能提速 500%!
前端·vue.js
慢慢长大的孩子1 小时前
个人运营小网站的最佳策略
前端·后端
牛奶2 小时前
ts随笔:基础与类型系统
前端·面试·typescript
用户73992986959722 小时前
DeepSeek/GPT-4 落地实战:我如何用 Node.js + AI 手搓一个“面试神器”
面试
牛奶2 小时前
JS随笔:浏览器 API(DOM 与 BOM)
前端·javascript·面试
用泥种荷花2 小时前
【LangChain.js学习】 会话记忆(临时/长期)全解析
前端
慢慢长大的孩子2 小时前
原生Android开发与JS桥开发对比分析
前端·后端