Axios TypeScript 类型系统详解:模块解析、错误 Type Guard 与泛型化请求配置(axios)
本文为 axios(Promise based HTTP client for the browser and node.js)TypeScript 支持的技术指南,围绕其官方文档 docs/fr/pages/advanced/type-script.md 的核心内容展开。读完本篇,你将掌握:axios 在 ESM/CJS 双模块格式下的 TypeScript 解析配置要点、如何用 isAxiosError / isCancel 对 catch 中的 unknown 错误做类型收窄,以及如何通过 AxiosRequestConfig<D, P> 双泛型、可调用 AxiosInstance、Symbol 键模块增强等机制实现请求/响应数据的端到端类型安全。
类型定义文件的发布方式
axios 通过 npm 包内置 TypeScript 类型声明:ESM 入口对应 index.d.ts,CommonJS 入口对应 index.d.cts,因此无论使用哪种模块格式,类型检查与编辑器补全开箱即用。这一点可以直接在 package.json 中得到验证:
- 顶层字段
"types": "index.d.ts"与"typings": "./index.d.ts"声明了默认类型入口; exports字段中通过"require": "./index.d.cts"、"default": "./index.d.ts"的types条件,把类型声明按模块解析方式精确路由到对应文件;- 运行时入口同样分离:
require指向./dist/node/axios.cjs,默认指向 ESM 的./index.js。
正是这种「ESM 默认导出 + CJS module.exports」的双发布形态,引出了下面这组配置注意事项。
模块解析的配置要点
由于 axios 同时发布 ESM 默认导出和 CJS 的 module.exports,TypeScript 项目需要留意以下解析细节(以下配置要求以文档为准,推荐 TypeScript 4.7+):
| 你的场景 | 建议配置 |
|---|---|
| 推荐方案 | "moduleResolution": "node16"(由 "module": "node16" 隐含启用),要求 TypeScript ≥ 4.7 |
| 项目本身使用 ESM | 通常无需额外调整,现有配置即可正确解析 |
编译到 CJS 且无法使用 node16 解析 |
必须开启 esModuleInterop,否则默认导入的互操作类型会报错 |
| 用 TypeScript 检查 CJS 格式的 JavaScript 代码 | 唯一可行的选项是 "moduleResolution": "node16" |
其原理对应 exports 映射:node16 解析器会依据你的文件是 ESM 还是 CJS 来选择 index.d.ts / index.d.cts,并正确处理默认导出互操作;而旧的 node(node10)解析器只能看到 types 顶层字段,无法感知条件导出,所以才需要 esModuleInterop 兜底。
错误类型守卫:isAxiosError 与 isCancel
在 catch 块中,error 的静态类型是 unknown。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>;
使用 axios.isAxiosError 收窄之后,即可在完全类型安全的状态下访问 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:
export default function isAxiosError(payload) {
return utils.isObject(payload) && payload.isAxiosError === true;
}
它检查对象上是否存在 isAxiosError === true 标记位,而该标记正对应 index.d.ts 中 AxiosError 类的 isAxiosError: boolean 属性——类型系统与运行时行为严格一致。
对于请求取消(例如使用 AbortController 的 signal 场景),用 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);
}
}
从类型声明看(index.d.ts),CanceledError<T, D, P> 继承自 AxiosError<T, D, P>,带有 name: 'CanceledError' 与可选的 __CANCEL__ 标记,因此收窄后可以访问与 AxiosError 相同的全部属性。AxiosError 上还以静态常量形式列出了常见的错误码(ERR_NETWORK、ETIMEDOUT、ECONNABORTED 等,见 index.d.ts),可用于 error.code 的枚举式比对。
请求数据与查询参数的双泛型:D 与 P
AxiosRequestConfig<D = any, P = any> 的两个泛型参数分别承载请求体数据(D)与查询参数(P),自定义的参数序列化器也能拿到同样的 P。该接口定义在 index.d.ts:
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;
// ...
}
注意 paramsSerializer 的条件类型 unknown extends P ? Record<string, any> : 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 },
};
这段代码演示了两个关键能力:
- 响应回传:默认响应中
response.config保留了D与P(AxiosResponse的config字段类型即InternalAxiosRequestConfig<D, P>,见 index.d.ts),即使请求是通过带类型的配置推断出来的别名方法,D、P依然被完整携带; - 编译期拦截错误:
params的类型是SearchParams,把query写成数字会在编译期被@ts-expect-error捕获。
除 AxiosResponse 与 AxiosPromise 外,RawAxiosRequestConfig、InternalAxiosRequestConfig、AxiosDefaults、CreateAxiosDefaults、AxiosError、CanceledError、可调用实例、适配器等类型,以及 mergeConfig() 函数(index.d.ts:mergeConfig<D, P>(config1, config2): AxiosRequestConfig<D, P>)也都会保留 P,即参数类型不会在整条配置链路上丢失。
请求方法新增的第四个泛型 P
请求方法(get/post/put 等)在原有三个泛型之后追加了 P,签名为 <T, R, D, P>(index.d.ts):
get<T = any, R = AxiosResponseDefault, D = any, P = any>(
url: string,
config?: AxiosRequestConfig<D, P>
): Promise<AxiosResponseResult<T, R, D, P>>;
T:响应数据类型;R:自定义响应类型(默认哨兵值AxiosResponseDefault表示使用标准AxiosResponse,见 index.d.ts 的AxiosResponseResult条件类型);D:请求数据;P:查询参数,默认any以保持向后兼容。
由于 P 追加在末尾,既有代码中 T、R、D 的位置不变,现有泛型写法无需迁移。显式提供的响应类型 R 仍然决定 Promise 的 resolve 值。
显式带类型的适配器(或任意 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
}
这里 AxiosPromise<T, D, P> 被声明为 Promise<AxiosResponse<T, D, {}, P>>(index.d.ts),错误守卫 isCancel<T, D, P> 收窄后也能读取带类型的 config.data / config.params,形成「请求 → 响应 → 错误」三个方向一致的类型闭环。
类型化的实例与拦截器
用 AxiosInstance 标注 axios.create 的返回值,并用 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 的定义(index.d.ts)在 Axios 类之上追加了可调用签名:apiClient<User>("/users/1") 与 apiClient<User>({ url: "/users/1" }) 两种写法都有完整泛型推导;create 返回的仍是 AxiosInstance,其 defaults 属性带有 headers 索引签名。默认导出被声明为 AxiosStatic(index.d.ts),它继承 AxiosInstance 并暴露 isCancel、isAxiosError、mergeConfig、toFormData、CanceledError、AxiosHeaders 等工具,因此 axios.get、axios.interceptors 与上述静态函数共享同一套类型。
使用 Symbol 键扩展请求配置
axios 在合并默认配置与单次请求配置时,会保留自有且可枚举的 Symbol 属性。这一行为的实现证据在 lib/core/mergeConfig.js:
if (Object.getOwnPropertySymbols && Object.getOwnPropertyDescriptor) {
return Object.keys(thing).concat(
Object.getOwnPropertySymbols(thing).filter(
(symbol) => Object.getOwnPropertyDescriptor(thing, symbol).enumerable
)
);
}
即:只有「自有(own)且可枚举(enumerable)」的 Symbol 属性会参与拷贝;继承来的或非可枚举的 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 });
这种方案的优势在于键名是 unique symbol,不会与公共配置字段冲突,且 declare module "axios" 的增强会全局生效于项目内所有引用 axios 类型的文件。
响应数据的泛型标注
axios 的请求方法针对响应数据类型是泛型的。给 axios.get<T>(以及其他别名方法)传入类型参数即可为 response.data 标注类型:
interface User {
id: number;
name: string;
}
const { data } = await apiClient.get<User>("/users/1");
// `data` 的类型为 `User`
配合前面介绍的 D、P 参数,一个接口调用可以完整表达四种类别:T(响应)、R(自定义响应结构)、D(请求体)、P(查询参数),例如 apiClient.get<User, void, RequestBody, SearchParams>(url, config)。
参考与延伸阅读
- 官方文档(本文骨架):docs/fr/pages/advanced/type-script.md,英文版见 docs/pages/advanced/type-script.md
- 类型声明入口:index.d.ts(ESM)、index.d.cts(CJS)
- 双格式发布配置:package.json
- 错误守卫实现:lib/helpers/isAxiosError.js
- Symbol 属性合并实现:lib/core/mergeConfig.js
- TypeScript 入门示例:docs/pages/getting-started/examples/typescript.md
- 类型兼容测试(ESM/CJS 双格式下的 typings 校验):tests/module/esm/tests/typings.module.test.js、tests/module/cjs/tests/typings.module.test.cjs
适用前提:本文的类型行为以当前仓库 package.json 中的 axios@1.19.0 声明文件为准;moduleResolution: node16 要求 TypeScript 4.7 及以上版本,低版本编译器请按前文表格选择 esModuleInterop 等替代配置。
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