首页
/ Axios TypeScript 类型系统实战指南:从双类型定义分发到类型安全的请求、错误与自定义配置

Axios TypeScript 类型系统实战指南:从双类型定义分发到类型安全的请求、错误与自定义配置

2026-09-06 21:59:07作者:苗圣禹Peter

Axios(npm 包 axios,当前仓库版本 1.19.0)在 npm 包内直接携带 TypeScript 类型定义:ESM 格式使用 index.d.ts,CJS 格式使用 index.d.cts,两种模块格式开箱即用地支持类型检查与编辑器智能提示。读完本篇,你能掌握 Axios 在 TypeScript 项目中的模块解析配置要点、错误类型守卫(isAxiosError / isCancel)、请求数据与查询参数的 D/P 泛型体系、类型化实例与拦截器、以及基于 Symbol 键的自定义请求配置扩展,并理解这些类型能力背后的源码实现。

一、类型定义的载体:index.d.ts 与 index.d.cts

Axios 是一个"双发布"(dual-publish)包:既提供 ESM 默认导出,也提供 CJS 的 module.exports。类型文件也因此存在两份,由 package.jsonexports 字段按导入条件自动路由:

  • exports["."].types.require 指向 ./index.d.cts,供 require(CJS)场景使用;
  • exports["."].types.default 指向 ./index.d.ts,供 ESM 及其他场景使用;
  • 顶层 types / typings 字段同样声明为 ./index.d.ts(见 package.json)。

index.d.ts 首行即标注了最低 TypeScript 版本要求:

// TypeScript Version: 4.7

该文件(约 787 行)完整声明了 Axios 的公共 API 表面,包括 AxiosHeaders 类、AxiosRequestConfigAxiosErrorCanceledErrorAxiosInstance 等。仓库自身的 tsconfig.json 也是一个可直接参考的配置样例:

{
  "compilerOptions": {
    "module": "node16",
    "lib": ["dom", "es2015"],
    "types": [],
    "strict": true,
    "noEmit": true
  }
}

二、模块解析配置要点(Module Resolution Caveats)

由于 ESM/CJS 双发布的存在,项目自身的 tsconfig.json 配置有若干需要注意的地方。官方建议按以下优先级选择:

  1. 推荐设置是 "moduleResolution": "node16"(由 "module": "node16" 隐含),该模式要求 TypeScript 4.7 或更高版本。这与 index.d.ts 标注的最低版本一致,也是仓库自己采用的方案。
  2. 如果你使用的是 ESM,现有配置通常没有问题。
  3. 如果你把 TypeScript 编译为 CJS 且无法使用 "moduleResolution": "node16",必须启用 esModuleInterop,否则默认导入(import axios from "axios")会因 CJS 的 module.exports 结构而报错。
  4. 如果你用 TypeScript 对 CJS JavaScript 代码做类型检查(allowJs / checkJs 场景),唯一可行的选项是 "moduleResolution": "node16"

这些约束的根源在于 Node 的 exports 条件解析:requireimport 命中的类型入口不同,只有在 Node 16+ 语义的模块解析下,TypeScript 才能与 Node 运行时一致地选对 index.d.ctsindex.d.ts

三、Axios 错误的类型守卫

3.1 axios.isAxiosError:安全收窄 unknown 错误

catch 块中 error 的静态类型是 unknown。Axios 提供了 isAxiosError 类型守卫对其进行安全收窄。从 index.d.ts 可以看到它的完整签名:

export function isAxiosError<T = any, D = any, P = any>(
  payload: any
): payload is AxiosError<T, D, P>;

收窄之后即可访问 error.responseerror.configerror.code 等 Axios 专属属性并获得完整类型支持:

import axios from "axios";

let user: User | null = null;
try {
  const { data } = await axios.get("/user?ID=12345");
  user = data.userDetails;
} catch (error) {
  if (axios.isAxiosError(error)) {
    handleAxiosError(error);
  } else {
    handleUnexpectedError(error);
  }
}

底层实现同样很简洁:lib/helpers/isAxiosError.js 判断 utils.isObject(payload) && payload.isAxiosError === true,而 AxiosError 类在 index.d.ts 中声明了 isAxiosError: boolean 标志及 configcoderequestresponsestatuscause 等字段,还定义了 ERR_NETWORKECONNABORTEDETIMEDOUTERR_CANCELED 等静态错误码常量,便于在守卫收窄后做错误分支处理。

3.2 axios.isCancel<T>():收窄取消错误

配合 AbortController 等取消机制,isCancel<T>() 类型守卫可把取消错误收窄为 CanceledError<T>

const controller = new AbortController();

try {
  await axios.get<User>("/user?ID=12345", { signal: controller.signal });
} catch (error) {
  if (axios.isCancel<User>(error)) {
    handleCancellation(error);
  }
}

其类型签名为 isCancel<T = any, D = any, P = any>(value: any): value is CanceledError<T, D, P>(见 index.d.ts)。运行时的判断依据见 lib/cancel/isCancel.jsreturn !!(value && value.__CANCEL__),对应 CanceledError 类上声明的 __CANCEL__? 属性与只读的 name: 'CanceledError'(见 index.d.tslib/cancel/CanceledError.js)。

四、请求数据与查询参数的类型化:DP 泛型

这是当前 Axios 类型体系中最值得掌握的部分。AxiosRequestConfig<D = any, P = any> 使用两个泛型参数:D 表示请求体数据data 字段),P 表示查询参数params 字段)——见 index.d.ts

export interface AxiosRequestConfig<D = any, P = any> {
  url?: string;
  method?: StringLiteralsOrString<Method>;
  baseURL?: string;
  params?: P;
  paramsSerializer?:
    | ParamsSerializerOptions<unknown extends P ? Record<string, any> : P>
    | CustomParamsSerializer<unknown extends P ? Record<string, any> : P>;
  data?: D;
  // ...
}

注意 paramsSerializer 的类型设计:当 P 未被显式指定(unknown extends P 成立)时退回 Record<string, any>;一旦显式指定了 P,自定义序列化函数接收到的就是同一种 P,参数得到完整类型检查。

一个端到端示例:

import axios, {
  type AxiosPromise,
  type AxiosRequestConfig,
  type InternalAxiosRequestConfig,
} from "axios";

interface RequestBody {
  includeArchived: boolean;
}

interface SearchParams {
  query: string;
  page?: number;
}

interface SearchResponse {
  results: string[];
}

const searchConfig: AxiosRequestConfig<RequestBody, SearchParams> = {
  data: { includeArchived: false },
  params: { query: "axios", page: 1 },
  paramsSerializer: (params) => `${params.query}:${params.page ?? 1}`,
};

const response = await axios.get("/search", searchConfig);
response.config.data;   // RequestBody | undefined
response.config.params; // SearchParams | undefined

const invalidConfig: AxiosRequestConfig<RequestBody, SearchParams> = {
  // @ts-expect-error `query` must be a string
  params: { query: 123 },
};

4.1 类型沿调用链的传递

默认的请求结果会把 DP 保留在 response.config 上,包括当请求别名(如 getpost)从类型化的请求配置中推断出这两个类型时。这一能力来源于 AxiosResponse 的定义——其 config 字段直接是携带 D/PInternalAxiosRequestConfig(见 index.d.ts):

export interface AxiosResponse<T = any, D = any, H = {}, P = any> {
  data: T;
  status: number;
  statusText: string;
  headers: (H & RawAxiosResponseHeaders) | AxiosResponseHeaders;
  config: InternalAxiosRequestConfig<D, P>;
  request?: any;
}

同一条类型链上,RawAxiosRequestConfigInternalAxiosRequestConfigAxiosDefaultsCreateAxiosDefaultsAxiosResponseAxiosPromiseAxiosErrorCanceledError、可调用实例(callable instance)、adapter 以及 mergeConfig() 都携带了 params 类型。其中 mergeConfig 的签名为 mergeConfig<D = any, P = any>(config1, config2): AxiosRequestConfig<D, P>(见 index.d.ts),保证配置合并后类型不丢失。

4.2 请求方法的泛型位置

请求方法以 <T, R, D, P> 四个泛型参数声明,P 作为新增的最后一个泛型,因此既有的响应数据(T)、自定义响应(R)和请求数据(D)的位置保持不变,显式提供的自定义响应类型仍然控制最终解析值;P 默认 any 以兼容旧代码。以 get 为例(见 index.d.ts):

get<T = any, R = AxiosResponseDefault, D = any, P = any>(
  url: string,
  config?: AxiosRequestConfig<D, P>
): Promise<AxiosResponseResult<T, R, D, P>>;

返回值类型由条件类型 AxiosResponseResult<T, R, D, P> 计算:R 为默认的 AxiosResponseDefault(一个 unique symbol)时解析为 AxiosResponse<T, D, {}, P>,否则采用自定义的 R(见 index.d.ts)。

4.3 在 adapter 与取消守卫中保持双类型

自定义 adapter 或显式类型的 promise 可以完整保留两组请求类型:

const searchAdapter = (
  config: InternalAxiosRequestConfig<RequestBody, SearchParams>
): AxiosPromise<SearchResponse, RequestBody, SearchParams> =>
  Promise.resolve({
    data: { results: [] },
    status: 200,
    statusText: "OK",
    headers: {},
    config,
  });

declare const error: unknown;

if (axios.isCancel<SearchResponse, RequestBody, SearchParams>(error)) {
  error.config?.data;   // RequestBody | undefined
  error.config?.params; // SearchParams | undefined
}

isCancel 的三个泛型位置(T 响应数据、D 请求数据、P 查询参数)与 AxiosError / CanceledError 的泛型定义(index.d.ts)一一对应,因此收窄后 error.config?.dataerror.config?.params 都能得到精确类型。

五、类型化实例与拦截器

axios.create 的结果标注 AxiosInstance 类型、给请求拦截器标注 InternalAxiosRequestConfig,即可为自定义客户端建立端到端的类型检查:

import axios, { AxiosInstance, InternalAxiosRequestConfig } from "axios";

const apiClient: AxiosInstance = axios.create({
  baseURL: "https://api.example.com",
  timeout: 10000,
});

apiClient.interceptors.request.use((config: InternalAxiosRequestConfig) => {
  // Add auth token, log, etc.
  return config;
});

从源码结构看,AxiosInstanceindex.d.ts 中是一个"可扩展的 Axios":它继承了 Axios 类的所有方法(get/post/postForm/query 等),同时自身是一个可调用接口((config) => Promise<...>(url, config) => Promise<...> 两个重载),并能再次 create() 派生实例。InternalAxiosRequestConfigAxiosRequestConfig 的差异只有一个——headers 从可选的原始头对象升级为必填的 AxiosRequestHeaders(见 index.d.ts),因此拦截器内 config.headers.set(...) 等调用都是类型安全的。AxiosStatic(顶层 axios 常量的类型,见 index.d.ts)则在其上聚合了 isCancelisAxiosErrorAxiosHeadersmergeConfigHttpStatusCode 等命名导出。

六、Symbol 键的自定义请求配置

Axios 在合并默认配置与单次请求配置时,会保留自身可枚举的 Symbol 属性。应用可以通过模块扩展(module augmentation)为 AxiosRequestConfig 增加一个特定的 Symbol 键,然后在拦截器或 adapter 中从 InternalAxiosRequestConfig 读取该选项:

import axios from "axios";

export const someFlag: unique symbol = Symbol(
  "some flag used in request interceptor"
);

declare module "axios" {
  interface AxiosRequestConfig<D = any, P = any> {
    [someFlag]?: boolean;
  }
}

axios.interceptors.request.use((config) => {
  if (config[someFlag]) {
    config.headers.set("X-Some-Flag", "enabled");
  }
  return config;
});

await axios.get("/users", { [someFlag]: true });

这一机制的源码依据在通用深度合并函数 lib/utils.jsmerge 中:

const symbols = Object.getOwnPropertySymbols(source);
for (let j = 0; j < symbols.length; j++) {
  const symbol = symbols[j];
  if (propertyIsEnumerable.call(source, symbol)) {
    assignValue(source[symbol], symbol);
  }
}

即逐个遍历源对象的自身 Symbol 属性,且仅当 propertyIsEnumerable 为真时才复制到合并结果。两条边界需要注意:

  • 只复制自身可枚举的 Symbol 属性,非可枚举属性和继承(原型链上的)Symbol 属性都不会被复制;
  • 字符串键的合并支持大小写无关查找(findKey),而 Symbol 键是恒等匹配(identity-matched)(见 lib/utils.js 的注释),因此不同 Symbol() 值之间的键不会互相覆盖。

由于 Symbol 键不会出现在 mergeMap 的按名合并逻辑(lib/core/mergeConfig.jsmergeMap 只列举了字符串键)中,它会走通用的深度合并路径并被完整保留——这正是"用 Symbol 键携带自定义请求选项"能够跨 mergeConfig 存活的原因。

七、响应数据的类型化

请求方法本身是泛型化的,向 axios.get<T>(以及其他别名方法)传入类型参数即可为 response.data 指定类型:

interface User {
  id: number;
  name: string;
}

const { data } = await apiClient.get<User>("/users/1");
// `data` 是 User 类型

这一能力对应上一节所述的 get<T, R, D, P> 签名中的第一个泛型位置:T 决定 AxiosResponse<T, ...>data 字段类型;transformResponse 的默认实现把 JSON 解析结果直接放入 data,因此 T 通常就是接口返回的 JSON 结构类型。若接口返回的不是 JSON(例如 responseType: "blob" / "arraybuffer",这些取值在 index.d.tsResponseType 联合类型中声明),把 T 标注为对应的二进制类型即可。

八、小结:类型 API 速查

类型 / 函数 定义位置 用途
AxiosRequestConfig<D, P> index.d.ts 请求配置,D=请求体、P=查询参数
InternalAxiosRequestConfig<D, P> index.d.ts 拦截器/adapter 中的配置,headers 必填
AxiosResponse<T, D, H, P> index.d.ts 响应类型,config 携带 D/P
AxiosError<T, D, P> / CanceledError<T, D, P> index.d.ts 错误类型,含 config/code/response
isAxiosError<T, D, P> index.d.ts 收窄 unknownAxiosError
isCancel<T, D, P> index.d.ts 收窄为 CanceledError
AxiosInstance index.d.ts 类型化的 axios 实例(可调用、可再 create
AxiosStatic index.d.ts 顶层 axios 常量的完整类型
create(config?: CreateAxiosDefaults) index.d.ts 创建携带默认配置(含 D/P)的实例

配置层面记住三句话:能上 "moduleResolution": "node16" 就上(要求 TypeScript ≥ 4.7);ESM 场景通常无需改动;编译到 CJS 且无法使用 node16 解析时必须开启 esModuleInterop。类型层面则用 D/P 泛型约束请求体与查询参数,用 AxiosInstance + InternalAxiosRequestConfig 打通实例与拦截器的类型链,需要携带私有请求选项时采用"Symbol 键 + declare module "axios" 模块扩展"的组合,即可获得一条从配置到错误处理全部类型安全的 Axios 使用链路。

登录后查看全文
热门项目推荐
相关项目推荐