axios TypeScript 类型系统实战:从模块解析、类型守卫到带泛型参数的请求配置
本文围绕 axios 官方文档 type-script.md 讲解的类型支持体系展开,覆盖 ESM/CJS 双模块下的类型解析配置、isAxiosError / isCancel 类型守卫的错误收窄、请求数据与查询参数的双泛型参数化,以及带类型的实例、拦截器和 Symbol 键自定义配置等场景。读完后你可以为 axios 客户端建立端到端的类型检查:从请求配置、拦截器、适配器一路到响应与错误处理,全部获得编译期保障。
随包提供的类型定义:ESM 与 CJS 双通道
axios 在 npm 包中通过 index.d.ts(ESM)和 index.d.cts(CJS)随包提供 TypeScript 类型定义,因此两种模块格式下的类型检查与编辑器支持都开箱即用。这一点在 package.json 的 exports 字段中有明确体现:
{
"exports": {
".": {
"types": {
"require": "./index.d.cts",
"default": "./index.d.ts"
},
...
}
}
}
- 顶层
types/typings字段均指向index.d.ts,作为旧版解析方式的兜底; exports条件映射中,require条件命中index.d.cts,default条件命中index.d.ts,与运行时入口(CJS 产物dist/node/axios.cjs与 ESM 入口 index.js)一一对应;- index.js 本身负责把默认导出解包为具名导出(
create、Axios、AxiosError、isCancel、isAxiosError、mergeConfig等),保证 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) 中的 error 在 useUnknownInCatchVariables 下是 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.response、error.config 和 error.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_RESPONSE、ERR_NETWORK、ETIMEDOUT 等错误码常量,配合 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);
}
}
CanceledError 是 AxiosError 的子类,声明位于 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.query是string;- 响应在
response.config上保留了D与P——AxiosResponse的config字段声明为InternalAxiosRequestConfig<D, P>(index.d.ts),类型信息因此能跟随响应对象流转。
类型的贯穿:哪些类型保留了 P
默认请求结果会在 response.config 上保留 D 和 P,包括请求别名从带类型的请求配置中推断出这些类型的情况。从 index.d.ts 的声明结构看,以下类型都声明了 <D = any, P = any>(或等效的)泛型参数,使类型贯穿整条调用链:
RawAxiosRequestConfig——它是AxiosRequestConfig的别名(index.d.ts);InternalAxiosRequestConfig<D, P> extends AxiosRequestConfig<D, P>,额外将headers收窄为AxiosRequestHeaders(index.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 可以同时保留 D 与 P:
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,收窄后的 CanceledError 的 config 依然携带 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: AxiosRequestHeaders(index.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 字段,status、statusText、headers、config 等字段保持独立类型,因此你既可以对 data 做精细建模,也不会影响其余响应字段的检查。
小结
axios 的类型系统围绕三个层次组织:
- 模块层:
index.d.ts/index.d.cts双入口配合exports条件映射,要求moduleResolution: "node16"(TypeScript 4.7+)或在 CJS 编译下启用esModuleInterop; - 错误处理层:
axios.isAxiosError与axios.isCancel两个类型守卫把unknown收窄为携带<T, D, P>泛型的AxiosError/CanceledError,运行时仅依赖isAxiosError/__CANCEL__等标记字段; - 请求配置层:
AxiosRequestConfig<D, P>双泛型参数贯穿AxiosDefaults、CreateAxiosDefaults、AxiosResponse、AxiosPromise、适配器、mergeConfig()及拦截器,Symbol 键可通过模块扩充为配置扩展私有选项。
相关代码入口可按路径继续深入:类型声明 index.d.ts 与 index.d.cts、ESM 入口 index.js、错误判定实现 lib/helpers/isAxiosError.js、取消错误实现 lib/cancel/CanceledError.js,以及类型测试 tests/module/cjs/tests/typings.module.test.cjs 与 tests/module/esm/tests/typings.module.test.js。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00