首页
/ Axios 1.x 的 TypeScript 实战指南:模块解析、错误 Type Guard、请求泛型 <D, P> 与 Symbol 配置键

Axios 1.x 的 TypeScript 实战指南:模块解析、错误 Type Guard、请求泛型 <D, P> 与 Symbol 配置键

2026-09-04 09:03:06作者:裴麒琰

在 TypeScript 项目中接入 axios,最常见的两个问题是"模块解析配置导致默认导出报错"和"catch 块里 errorunknown 无法安全访问 responseconfig 等属性"。本文基于 axios 仓库内的 TypeScript 官方指南,结合 index.d.ts 类型定义与 lib/utils.js 合并逻辑源码,系统讲解:双模块格式下的 moduleResolution 配置要点、axios.isAxiosError / axios.isCancel 两个 type guard 的窄化用法、AxiosRequestConfig<D, P> 双泛型对请求体与查询参数的完整类型传递(含 paramsSerializer、适配器与 response.config)、AxiosInstance 类型化实例与拦截器标注,以及通过 declare module 扩充 Symbol 键自定义请求配置。读完本文,你可以在 ESM/CJS 双环境下正确配置 tsconfig,并对请求、响应、错误、取消全链路获得端到端的类型检查。

类型定义文件的分发方式:index.d.tsindex.d.cts

axios 在 npm 包中直接携带 TypeScript 定义:index.d.ts 面向 ESM,index.d.cts 面向 CJS,因此两种模块格式下的类型检查与编辑器智能提示均开箱即用,无需像某些库那样额外安装 @types/axios

从仓库根目录的 package.json 可以看到具体的分发机制(当前仓库版本为 1.19.0):

  • 顶层 "types": "index.d.ts""typings": "index.d.ts" 指向 ESM 定义文件;
  • exports["."].types 按解析条件拆分:"require" 命中 ./index.d.cts(CommonJS),"default" 命中 ./index.d.ts(ESM)。

这正是"双发布(dual-publish)"的形态:运行时 ESM 默认导出与 CJS module.exports 并存,类型文件也随之一分为二。理解这一点,是后面所有模块解析配置建议的前提。

模块解析(Module Resolution)配置注意事项

由于 axios 同时以 ESM 默认导出和 CJS module.exports 发布,tsconfig 中需要做相应取舍:

  • 推荐设置是 "moduleResolution": "node16"(由 "module": "node16" 隐式启用),要求 TypeScript 4.7 或更高版本;
  • 如果你的项目本身就是 ESM,默认设置通常已经没有问题;
  • 如果你把 TypeScript 编译为 CJS 且无法使用 "moduleResolution": "node16"必须启用 esModuleInterop,否则 import axios from "axios" 这类默认导入会因 CJS 端没有默认导出而报错;
  • 如果你用 TypeScript 对 CJS 风格的 JavaScript 代码做类型检查(checkJs 场景),唯一可行的选择就是 "moduleResolution": "node16"

一个典型的 tsconfig 配置示例:

{
  "compilerOptions": {
    "module": "node16",
    "moduleResolution": "node16",
    "esModuleInterop": true,
    "target": "ES2020"
  }
}

要点在于:moduleResolution 决定了 TS 编译器走哪条 exports 分支去加载类型文件,配置错时最典型的报错就是"模块没有默认导出"这类看似无解的类型错误——本质上是编译器解析到了与运行时不一致的入口。

用 Type Guard 安全窄化 axios 错误

axios.isAxiosError:窄化 unknown 错误

catch 块中,error 的默认类型是 unknown(或 any),直接访问 error.responseerror.configerror.code 既不推荐也不安全。使用 axios.isAxiosError 这个 type guard 窄化之后,你就可以在完整类型安全的前提下访问这些 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

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

也就是说,运行时该 guard 只是检查对象上的 isAxiosError === true 标记位;类型层面则由 index.d.tsAxiosError 类声明的 isAxiosError: boolean 属性(index.d.ts 第 537 行)配合 guard 的 value is AxiosError 签名完成窄化。AxiosError 类声明(index.d.ts 第 524-539 行)同时暴露了 configcoderequestresponsestatustoJSON() 等属性,窄化之后这些字段全部可安全访问,且类上还声明了 ERR_NETWORKERR_BAD_RESPONSEERR_CANCELED 等静态错误码常量,便于在 handleAxiosError 中按 error.code 分支处理。

axios.isCancel<T>():窄化取消错误到 CanceledError<T>

请求取消(例如通过 AbortSignal 中断)会以 CanceledError 形式抛出。用 axios.isCancel<T>() 可以把 unknown 错误窄化为 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);
  }
}

类型签名定义在 index.d.ts 第 755 行:

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

注意该 guard 本身接受三个泛型参数——响应数据 T 之外还有请求数据 D 与查询参数 P,这与下面要讲的请求泛型体系是一脉相承的。运行时判断依据见 lib/cancel/isCancel.js:检查 value.__CANCEL__ 标记。

请求数据与查询参数的类型化:AxiosRequestConfig<D, P> 双泛型

AxiosRequestConfig<D = any, P = any> 使用 D 表示请求体类型、P 表示查询参数类型;自定义参数序列化器(paramsSerializer)接收的也是同一个 P。这一点可以从 index.d.ts 第 391-402 行的接口声明中得到印证:

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

unknown extends P ? Record<string, any> : P 是一个条件类型技巧:当你显式给出 P 时,序列化器回调的参数就是 P;当 P 为默认的 any 时则退化为宽松的 Record<string, any>,从而保持向后兼容。

完整示例:

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

最后一段 @ts-expect-error 演示了窄化收益:query 声明为 string,传入数字 123 会直接报错。

泛型在整条链路上的传递

默认请求结果会在 response.config 上保留 DP——即使请求别名方法是从带类型的请求配置中推断出这些类型的。从 index.d.ts 的类型声明看,这条链路上的类型都携带参数泛型:

  • RawAxiosRequestConfig<D, P>AxiosRequestConfig 的别名)、InternalAxiosRequestConfig<D, P>
  • AxiosDefaults<D, P>CreateAxiosDefaults<D, P>
  • AxiosResponse<T, D, P>AxiosPromise
  • AxiosError<T, D, P>CanceledError<T, D, P>
  • 可调用实例(callable instances)、适配器(adapters)以及 mergeConfig<D, P>()(声明在 index.d.ts 第 759 行)。

请求方法则以追加方式P 加为最后一个泛型——<T, R, D, P>——这样既有的响应数据(T)、自定义响应(R)、请求数据(D)的位置都不变,显式提供的自定义响应类型仍然控制最终 resolve 的值;P 默认为 any 以保持向后兼容。

适配器与显式类型化的 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
}

这里 CanceledErrorconfig 字段类型为 InternalAxiosRequestConfig<D, P>,因此取消错误上也能追溯到完整的请求泛型——三个泛型参数顺序为 <T, 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) => {
  // 添加认证 token、记录日志等
  return config;
});

类型层面,AxiosInstance 继承自 Axiosindex.d.ts 第 710 行)并额外提供调用签名,因此既能 apiClient("/users") 也能 apiClient.get<User>("/users/1")。而 InternalAxiosRequestConfigAxiosRequestConfig 的关键区别在 index.d.ts 第 485-487 行:

export interface InternalAxiosRequestConfig<D = any, P = any> extends AxiosRequestConfig<D, P> {
  headers: AxiosRequestHeaders;
}

进入请求管道后 headers 已经是确定的 AxiosHeaders 对象(必填且不再是"原始头对象或 AxiosHeaders 的联合"),所以拦截器内可以放心使用 config.headers.set(...) 这样的对象方法,而不必再做判空或形态判断。

使用 Symbol 键扩展自定义请求配置

axios 在合并默认配置与单次请求配置时,会保留自身的、可枚举的 symbol 属性。因此应用可以通过模块增强(module augmentation)给 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 键的处理。在 lib/utils.jsmerge 函数中可以看到(第 614-626 行附近):先通过 forEach(source, assignValue) 遍历字符串键,随后显式执行 Object.getOwnPropertySymbols(source),且仅当 propertyIsEnumerable.call(source, symbol) 为真时才执行 assignValue(source[symbol], symbol)——即只有自身的、可枚举的 symbol 属性会被复制,非枚举或继承来的 symbol 属性不会。同时第 590-592 行的注释也说明了 caseless(大小写不敏感)查找仅适用于字符串键,symbol 键按身份(identity)精确匹配,不会发生键名折叠。

这个能力适合实现一些不想污染公共配置字段的内部标记位(如重试标记、灰度开关),并且类型系统可以通过 declare module "axios" 增强保持完整检查。

响应数据的类型化

Axios 的请求方法对响应数据类型是泛型的:向 axios.get<T>(以及其他别名方法)传入类型参数,即可给 response.data 赋予精确类型:

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

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

这一点与前面 axios.get<User>("/user?ID=12345", { signal }) 的取消示例中 T 的位置一致——T 始终控制响应数据,也就是 Promise 的 resolve 值(除非你通过 R 显式指定了自定义响应类型)。

小结与核对清单

场景 正确做法 依据
ESM 项目 保持默认即可;推荐 "moduleResolution": "node16"(TS ≥ 4.7) docs/es/pages/advanced/type-script.md
编译到 CJS 且不能用 node16 必须启用 esModuleInterop 同上
对 CJS JavaScript 做类型检查 只能用 "moduleResolution": "node16" 同上
catch 块处理 HTTP/网络错误 axios.isAxiosError(error) 窄化后访问 response/config/code lib/helpers/isAxiosError.js
处理请求取消 axios.isCancel<T>(error) 窄化为 CanceledError<T> lib/cancel/isCancel.jsindex.d.ts
类型化请求体/查询参数 AxiosRequestConfig<D, P>,请求方法泛型为 <T, R, D, P> index.d.ts
自定义客户端 AxiosInstance + 拦截器标注 InternalAxiosRequestConfig index.d.ts
内部标记位配置 declare module "axios" 增强 symbol 键;仅自身可枚举 symbol 会被合并保留 lib/utils.js

掌握以上内容后,你的 TypeScript + axios 项目从 tsconfig 配置到请求、响应、错误、取消的全链路都能获得静态检查,@ts-expect-error 可以成为回归类型约束的轻量"测试"手段。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384