Axios TypeScript 类型系统实战指南:从双类型定义分发到类型安全的请求、错误与自定义配置
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.json 的 exports 字段按导入条件自动路由:
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 类、AxiosRequestConfig、AxiosError、CanceledError、AxiosInstance 等。仓库自身的 tsconfig.json 也是一个可直接参考的配置样例:
{
"compilerOptions": {
"module": "node16",
"lib": ["dom", "es2015"],
"types": [],
"strict": true,
"noEmit": true
}
}
二、模块解析配置要点(Module Resolution Caveats)
由于 ESM/CJS 双发布的存在,项目自身的 tsconfig.json 配置有若干需要注意的地方。官方建议按以下优先级选择:
- 推荐设置是
"moduleResolution": "node16"(由"module": "node16"隐含),该模式要求 TypeScript 4.7 或更高版本。这与index.d.ts标注的最低版本一致,也是仓库自己采用的方案。 - 如果你使用的是 ESM,现有配置通常没有问题。
- 如果你把 TypeScript 编译为 CJS 且无法使用
"moduleResolution": "node16",必须启用esModuleInterop,否则默认导入(import axios from "axios")会因 CJS 的module.exports结构而报错。 - 如果你用 TypeScript 对 CJS JavaScript 代码做类型检查(
allowJs/checkJs场景),唯一可行的选项是"moduleResolution": "node16"。
这些约束的根源在于 Node 的 exports 条件解析:require 与 import 命中的类型入口不同,只有在 Node 16+ 语义的模块解析下,TypeScript 才能与 Node 运行时一致地选对 index.d.cts 或 index.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.response、error.config、error.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 标志及 config、code、request、response、status、cause 等字段,还定义了 ERR_NETWORK、ECONNABORTED、ETIMEDOUT、ERR_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.js:return !!(value && value.__CANCEL__),对应 CanceledError 类上声明的 __CANCEL__? 属性与只读的 name: 'CanceledError'(见 index.d.ts 和 lib/cancel/CanceledError.js)。
四、请求数据与查询参数的类型化:D 与 P 泛型
这是当前 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 类型沿调用链的传递
默认的请求结果会把 D 与 P 保留在 response.config 上,包括当请求别名(如 get、post)从类型化的请求配置中推断出这两个类型时。这一能力来源于 AxiosResponse 的定义——其 config 字段直接是携带 D/P 的 InternalAxiosRequestConfig(见 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;
}
同一条类型链上,RawAxiosRequestConfig、InternalAxiosRequestConfig、AxiosDefaults、CreateAxiosDefaults、AxiosResponse、AxiosPromise、AxiosError、CanceledError、可调用实例(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?.data 与 error.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;
});
从源码结构看,AxiosInstance 在 index.d.ts 中是一个"可扩展的 Axios":它继承了 Axios 类的所有方法(get/post/postForm/query 等),同时自身是一个可调用接口((config) => Promise<...> 与 (url, config) => Promise<...> 两个重载),并能再次 create() 派生实例。InternalAxiosRequestConfig 与 AxiosRequestConfig 的差异只有一个——headers 从可选的原始头对象升级为必填的 AxiosRequestHeaders(见 index.d.ts),因此拦截器内 config.headers.set(...) 等调用都是类型安全的。AxiosStatic(顶层 axios 常量的类型,见 index.d.ts)则在其上聚合了 isCancel、isAxiosError、AxiosHeaders、mergeConfig、HttpStatusCode 等命名导出。
六、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.js 的 merge 中:
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.js 中 mergeMap 只列举了字符串键)中,它会走通用的深度合并路径并被完整保留——这正是"用 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.ts 的 ResponseType 联合类型中声明),把 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 | 收窄 unknown 为 AxiosError |
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 使用链路。
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 StartedRust0626
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