首页
/ axios TypeScript 类型系统实战:从模块解析、类型守卫到带泛型参数的请求配置

axios TypeScript 类型系统实战:从模块解析、类型守卫到带泛型参数的请求配置

2026-09-06 13:40:18作者:咎竹峻Karen

本文围绕 axios 官方文档 type-script.md 讲解的类型支持体系展开,覆盖 ESM/CJS 双模块下的类型解析配置、isAxiosError / isCancel 类型守卫的错误收窄、请求数据与查询参数的双泛型参数化,以及带类型的实例、拦截器和 Symbol 键自定义配置等场景。读完后你可以为 axios 客户端建立端到端的类型检查:从请求配置、拦截器、适配器一路到响应与错误处理,全部获得编译期保障。

随包提供的类型定义:ESM 与 CJS 双通道

axios 在 npm 包中通过 index.d.ts(ESM)和 index.d.cts(CJS)随包提供 TypeScript 类型定义,因此两种模块格式下的类型检查与编辑器支持都开箱即用。这一点在 package.jsonexports 字段中有明确体现:

{
  "exports": {
    ".": {
      "types": {
        "require": "./index.d.cts",
        "default": "./index.d.ts"
      },
      ...
    }
  }
}
  • 顶层 types / typings 字段均指向 index.d.ts,作为旧版解析方式的兜底;
  • exports 条件映射中,require 条件命中 index.d.ctsdefault 条件命中 index.d.ts,与运行时入口(CJS 产物 dist/node/axios.cjs 与 ESM 入口 index.js)一一对应;
  • index.js 本身负责把默认导出解包为具名导出(createAxiosAxiosErrorisCancelisAxiosErrormergeConfig 等),保证 ESM 与 CJS 消费方看到的 API 形状一致。

当前仓库的 typescript devDependency 为 ^5.9.3,说明项目自身在较新的 TypeScript 版本下维护类型定义。

模块解析注意事项

由于 axios 同时以 ESM 默认导出和 CJS module.exports 两种方式发布,存在以下 tsconfig 配置注意事项:

  • 推荐使用 "moduleResolution": "node16"(由 "module": "node16" 隐式指定),需要 TypeScript 4.7 或更高版本;
  • 如果你使用 ESM,现有配置应该没有问题;
  • 如果你将 TypeScript 编译为 CJS 且无法使用 "moduleResolution": "node16",则必须启用 esModuleInterop
  • 如果你使用 TypeScript 对 CJS JavaScript 代码进行类型检查,则只能使用 "moduleResolution": "node16"

这些约束的根源在于 exports 映射的 types 条件分支:只有 node16/nodenext(或 bundler)等解析策略会正确读取条件导出中的 require/default 分支,把 CJS 用法映射到 index.d.cts、ESM 用法映射到 index.d.ts;旧的 node(node10)解析则只认顶层 types 字段。

axios 错误的类型守卫

isAxiosError:在 catch 中收窄 unknown 错误

catch (error) 中的 erroruseUnknownInCatchVariables 下是 unknown 类型,无法直接访问 axios 专有属性。使用 axios.isAxiosError 类型守卫可以安全地收窄它:

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);
  }
}

收窄之后,你便可以在完整的类型支持下访问 error.responseerror.configerror.code 等 axios 专有属性。

从类型声明看,index.d.ts 中的签名为:

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

export function isCancel<T = any, D = any, P = any>(value: any): value is CanceledError<T, D, P>;

两个守卫都携带 <T, D, P> 三组泛型参数,与后文请求数据的类型贯穿能力保持一致。

运行时实现非常轻量,见 lib/helpers/isAxiosError.js

export default function isAxiosError(payload) {
  return utils.isObject(payload) && payload.isAxiosError === true;
}

即判断依据是错误对象上的 isAxiosError === true 标记。对应地,AxiosError 类声明中包含 isAxiosError: boolean 字段(index.d.ts),并静态暴露 ERR_BAD_RESPONSEERR_NETWORKETIMEDOUT 等错误码常量,配合 code 字段可做细粒度的错误分支。

isCancel:收窄取消错误

使用 axios.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);
  }
}

CanceledErrorAxiosError 的子类,声明位于 index.d.ts,携带 name: 'CanceledError'__CANCEL__ 标记。由于取消请求同样会经过完整的请求链路,isCancel 的泛型参数可以一路传递到后文介绍的适配器与错误场景。

为请求数据和查询参数添加类型

双泛型参数:D 与 P

AxiosRequestConfig<D = any, P = any> 使用 D 表示请求数据(对应 data?: D 字段),使用 P 表示查询参数(对应 params?: P 字段)。完整的接口定义见 index.d.ts,其中与类型化直接相关的字段包括:

export interface AxiosRequestConfig<D = any, P = any> {
  url?: string;
  method?: StringLiteralsOrString<Method>;
  baseURL?: string;
  data?: D;
  params?: P;
  paramsSerializer?:
    | ParamsSerializerOptions<unknown extends P ? Record<string, any> : P>
    | CustomParamsSerializer<unknown extends P ? Record<string, any> : P>;
  // ...省略其余字段(timeout、headers、adapter 等)
}

注意 paramsSerializer 的签名使用条件类型 unknown extends P ? Record<string, any> : P:当你显式给出 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` 必须是字符串
  params: { query: 123 },
};

关键点:

  • params: { query: 123 } 会在编译期报错,因为 SearchParams.querystring
  • 响应在 response.config 上保留了 DP——AxiosResponseconfig 字段声明为 InternalAxiosRequestConfig<D, P>index.d.ts),类型信息因此能跟随响应对象流转。

类型的贯穿:哪些类型保留了 P

默认请求结果会在 response.config 上保留 DP,包括请求别名从带类型的请求配置中推断出这些类型的情况。从 index.d.ts 的声明结构看,以下类型都声明了 <D = any, P = any>(或等效的)泛型参数,使类型贯穿整条调用链:

  • RawAxiosRequestConfig——它是 AxiosRequestConfig 的别名(index.d.ts);
  • InternalAxiosRequestConfig<D, P> extends AxiosRequestConfig<D, P>,额外将 headers 收窄为 AxiosRequestHeadersindex.d.ts),这也是拦截器中应标注的类型;
  • AxiosDefaults<D, P>CreateAxiosDefaults<D, P>(后者的 headers 放宽为 RawAxiosRequestHeaders | AxiosHeaders | Partial<HeadersDefaults>);
  • AxiosResponse<T, D, H, P>AxiosPromise<T, D, P>AxiosError<T, D, P>CanceledError<T, D, P>
  • 可调用实例、适配器和 mergeConfig()

请求方法将 P 添加为最后一个泛型参数,即 <T, R, D, P>,因此现有的响应数据(T)、自定义响应(R)和请求数据(D)参数位置保持不变。显式提供的自定义响应类型仍然控制最终返回值。为保持向后兼容,P 默认为 any

适配器与 isCancel 同时保留两种请求类型

适配器或其他显式添加类型的 Promise 可以同时保留 DP

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
}

这说明:即使是取消错误,只要按三组泛型 <T, D, P> 调用 isCancel,收窄后的 CanceledErrorconfig 依然携带 D/P 类型,错误路径上也不会丢失请求侧的类型信息。

带类型的实例与拦截器

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) => {
  // 添加认证令牌、记录日志等
  return config;
});

标注为 InternalAxiosRequestConfig 而非原始请求配置,是因为请求拦截器运行时拿到的是已经合并默认配置、且 headers 已实例化为 AxiosHeaders 对象的配置——这正对应类型声明中 InternalAxiosRequestConfig 相对 AxiosRequestConfig 的唯一差异:headers: AxiosRequestHeadersindex.d.ts)。因此拦截器内可以直接使用 config.headers.set(...) 这类方法,而不需要处理原始头部对象的形态。

使用 Symbol 键的自定义请求配置

axios 合并默认配置和单次请求配置时,会保留自身的、可枚举的 Symbol 属性。应用可以通过模块扩充为 AxiosRequestConfig 添加特定的 Symbol 键,然后在拦截器或适配器的 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 });

这条机制的边界需要注意:只有自身的、可枚举的 Symbol 属性会被复制;不可枚举或继承的 Symbol 属性不会被复制。这意味着该能力适合在 axios.get 等调用处以内联字面量对象传入(对象字面量属性默认可枚举),而不适合依赖 Object.defineProperty 定义的不可枚举属性或在原型上设置的属性。

为响应数据添加类型

axios 的请求方法对响应数据类型是泛型的。向 axios.get<T>(以及其他别名)传入类型参数即可为 response.data 添加类型:

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

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

结合 AxiosResponse<T = any, D = any, H = {}, P = any> 的声明(index.d.ts)可以看出,T 只作用于 data 字段,statusstatusTextheadersconfig 等字段保持独立类型,因此你既可以对 data 做精细建模,也不会影响其余响应字段的检查。

小结

axios 的类型系统围绕三个层次组织:

  1. 模块层index.d.ts / index.d.cts 双入口配合 exports 条件映射,要求 moduleResolution: "node16"(TypeScript 4.7+)或在 CJS 编译下启用 esModuleInterop
  2. 错误处理层axios.isAxiosErroraxios.isCancel 两个类型守卫把 unknown 收窄为携带 <T, D, P> 泛型的 AxiosError / CanceledError,运行时仅依赖 isAxiosError / __CANCEL__ 等标记字段;
  3. 请求配置层AxiosRequestConfig<D, P> 双泛型参数贯穿 AxiosDefaultsCreateAxiosDefaultsAxiosResponseAxiosPromise、适配器、mergeConfig() 及拦截器,Symbol 键可通过模块扩充为配置扩展私有选项。

相关代码入口可按路径继续深入:类型声明 index.d.tsindex.d.cts、ESM 入口 index.js、错误判定实现 lib/helpers/isAxiosError.js、取消错误实现 lib/cancel/CanceledError.js,以及类型测试 tests/module/cjs/tests/typings.module.test.cjstests/module/esm/tests/typings.module.test.js

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