axios TypeScript 类型系统实战:泛型请求、类型化实例、类型守卫与 ESM/CJS 模块解析配置
axios 原生内置 TypeScript 类型定义,无需依赖 @types/axios 第三方包。本文以 axios 仓库中“TypeScript example”文档为主线,完整覆盖类型导入、泛型请求标注、函数/POST 请求类型封装、类型化实例与拦截器、错误类型收窄等核心操作,并结合 index.d.ts、package.json 与 lib/ 源码剖析背后的类型体系与双格式(ESM/CJS)分发机制,帮助你在 TypeScript 项目中获得端到端的类型安全。
原生内置类型:无需安装 @types 包
axios 的类型定义随 npm 包一起发布,通过 index.d.ts(ESM 入口)和 index.d.cts(CJS 入口)两套声明文件分别服务两种模块格式。从 package.json 的 exports 字段可以看到分发规则:
"exports": {
".": {
"types": {
"require": "./index.d.cts",
"default": "./index.d.ts"
},
...
"default": {
"require": "./dist/node/axios.cjs",
"default": "./index.js"
}
}
}
也就是说:用 import 引入时会拿到 ESM 入口 ./index.js 与类型文件 index.d.ts;用 require 引入时会拿到 CJS 构建产物 ./dist/node/axios.cjs 与类型文件 index.d.cts。这就是文档中强调“axios dual-publishes ESM and CJS(双格式发布)”的出处,也是后文 tsconfig 注意事项的根源。
另外 index.d.ts 第一行声明了 // TypeScript Version: 4.7,与文档中“推荐配置要求 TypeScript 4.7 或更高版本”的说法相互印证。
导入类型(Importing types)
axios 把所有核心类型从包根路径导出,你可以按需直接导入:
import axios from "axios";
import type { AxiosRequestConfig, AxiosResponse, AxiosError } from "axios";
这些类型在 index.d.ts 中均有完整声明,其中与日常使用最相关的几个包括:
| 类型 | 定义位置 | 用途 |
|---|---|---|
AxiosRequestConfig<D, P> |
index.d.ts | 请求配置,D 为请求体类型、P 为查询参数类型 |
InternalAxiosRequestConfig<D, P> |
index.d.ts | 拦截器中拿到的配置,headers 为必填的 AxiosRequestHeaders |
AxiosResponse<T, D, H, P> |
index.d.ts | 响应对象,T 即 response.data 的类型 |
AxiosError<T, D, P> |
index.d.ts | 错误对象,携带 response、config、code 等属性 |
AxiosInstance |
index.d.ts | axios.create() 的返回类型,可调用接口 |
AxiosStatic |
index.d.ts | 默认导出 axios 对象本身的类型 |
值得注意的是 InternalAxiosRequestConfig 的定义:
export interface InternalAxiosRequestConfig<D = any, P = any> extends AxiosRequestConfig<D, P> {
headers: AxiosRequestHeaders;
}
它继承自 AxiosRequestConfig,但把 headers 从可选(RawAxiosRequestHeaders & MethodsHeaders | AxiosHeaders)收紧为必填的 AxiosRequestHeaders。原因是进入请求拦截器时,axios 已经完成默认 headers 与请求 headers 的合并,此时 headers 一定是一个可用的 AxiosHeaders 对象——这个设计直接决定了下文拦截器的写法。
为请求标注类型(Typing a request)
在请求方法上使用泛型参数,即可告诉 TypeScript 响应数据的形状:
import axios from "axios";
type Post = {
userId: number;
id: number;
title: string;
body: string;
};
const response = await axios.get<Post>("https://jsonplaceholder.typicode.com/posts/1");
console.log(response.data.title); // TypeScript knows this is a string
对应到类型声明,get 的签名为:
get<T = any, R = AxiosResponseDefault, D = any, P = any>(
url: string,
config?: AxiosRequestConfig<D, P>
): Promise<AxiosResponseResult<T, R, D, P>>;
(见 index.d.ts)四个泛型的职责分别是:
T:response.data的类型,即上文示例中的Post;R:自定义整个响应形状,默认AxiosResponseDefault(唯一符号标记,解析为标准的AxiosResponse<T, D, {}, P>);D:请求体类型;P:查询参数params的类型,默认any。
因此 axios.get<Post>(...) 等价于声明 Promise<AxiosResponse<Post, any, {}, any>>,response.data 被精确推断为 Post,response.data.title 是 string 而非 any。delete、head、options、post、put、patch、postForm 等所有请求方法都遵循相同的 <T, R, D, P> 泛型模式(见 index.d.ts),用法一致。
为函数标注类型(Typing a function)
把请求封装进显式返回类型的函数,可以最大化类型安全性:
import axios, { AxiosResponse } from "axios";
type Post = {
userId: number;
id: number;
title: string;
body: string;
};
const getPost = async (id: number): Promise<Post> => {
const response = await axios.get<Post>(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
return response.data;
};
这里的类型链路是:axios.get<Post> 返回 AxiosResponse<Post>,response.data 即 Post,函数签名 Promise<Post> 与之吻合。调用方因此拿到的是干净的领域类型 Post,而不是包裹了 status/headers/config 的完整响应对象——把“响应壳”留在函数内部,把“数据”暴露给调用方,是 axios 类型化封装的常见做法。
为 POST 请求标注类型(Typing a POST request)
POST 场景下可以同时标注请求体与期望响应:
type CreatePostBody = {
title: string;
body: string;
userId: number;
};
type CreatePostResponse = CreatePostBody & { id: number };
const createPost = async (data: CreatePostBody): Promise<CreatePostResponse> => {
const response = await axios.post<CreatePostResponse>(
"https://jsonplaceholder.typicode.com/posts",
data
);
return response.data;
};
post 方法的签名为 post<T, R, D, P>(url, data?: D, config?)(见 index.d.ts)。如果希望请求体 data 也参与类型推导而非 any,可以在泛型第三个位置传入,或给 config 显式标注 AxiosRequestConfig<CreatePostBody, ...>——因为 AxiosRequestConfig 的 data?: D 字段与请求方法共享 D 这个类型槽位(见 index.d.ts)。此外,paramsSerializer 也会接收与 P 一致的参数类型,可参考仓库英文文档 TypeScript 页 中 “Typing request data and query params” 一节的完整示例。
类型化的 axios 实例(Typed axios instance)
创建类型化实例,可以把 baseURL、timeout、默认 headers 从第一步就固化为类型的一部分:
import axios from "axios";
import type { AxiosInstance } from "axios";
const api: AxiosInstance = axios.create({
baseURL: "https://api.example.com",
timeout: 5000,
});
axios.create 的返回类型是 AxiosInstance,它 extends Axios 并额外声明了可调用签名(见 index.d.ts):
export interface AxiosInstance extends Axios {
<T = any, R = AxiosResponseDefault, D = any, P = any>(
config: AxiosRequestConfig<D, P>
): Promise<AxiosResponseResult<T, R, D, P>>;
<T = any, R = AxiosResponseDefault, D = any, P = any>(
url: string,
config?: AxiosRequestConfig<D, P>
): Promise<AxiosResponseResult<T, R, D, P>>;
create(config?: CreateAxiosDefaults): AxiosInstance;
...
}
这意味着 api(url, config)、api.get<T>(url)、api.post<T, R, D>(url, data) 等所有调用形式都受类型检查,且实例可以链式 create 出携带更多默认值的子实例。
从源码看,axios.create 的实现在 lib/axios.js 中:
Axios.prototype.create = function create(instanceConfig) {
const mergeConfig = (config1, config2) => mergeConfigWithContext(...);
// ...
const instance = bind(Axios.prototype.request, context);
// ...
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};
即:新实例 = 默认配置与传入配置经 mergeConfig 合并后,通过 createInstance 生成;默认导出 axios 本身也是 createInstance(defaults) 的结果(见 lib/axios.js)。这也解释了为什么实例与 axios 静态对象拥有相同的 API 面:二者是同一 Axios 类的不同实例。另外 lib/axios.js 中 axios.default = axios; 这一行,是为 CJS esModuleInterop 场景下的默认导入兜底,与下文编译器配置注意事项直接相关。
类型化的拦截器(Typed interceptors)
v1.x 中,请求拦截器应使用 InternalAxiosRequestConfig(而不是 AxiosRequestConfig):
import axios from "axios";
import type { InternalAxiosRequestConfig, AxiosResponse } from "axios";
api.interceptors.request.use((config: InternalAxiosRequestConfig) => {
config.headers.set("Authorization", `Bearer ${getToken()}`);
return config;
});
api.interceptors.response.use(
(response: AxiosResponse) => response,
(error) => Promise.reject(error)
);
为什么必须区分这两个类型?从类型声明看,AxiosInterceptorManager 按场景分发不同的 use 签名(见 index.d.ts):
type AxiosRequestInterceptorUse<T> = (
onFulfilled?: AxiosInterceptorFulfilled<T> | null,
onRejected?: AxiosInterceptorRejected | null,
options?: AxiosInterceptorOptions
) => number;
请求拦截器的 T 固定为 InternalAxiosRequestConfig,响应拦截器的 T 固定为 AxiosResponse。如果你误用 AxiosRequestConfig 标注请求拦截器参数,config.headers.set(...) 会编译报错——因为 AxiosRequestConfig.headers 是可选的,且可能是 RawAxiosRequestHeaders 这类尚未初始化的原始对象;而 InternalAxiosRequestConfig.headers 是必填的 AxiosRequestHeaders,即 AxiosHeaders 实例,set、get、normalize 等方法(见 index.d.ts 中的 AxiosHeaders 类声明)才能被正确调用。
拦截器的运行时行为可在 lib/core/InterceptorManager.js 中印证:use(fulfilled, rejected, options) 注册处理器并返回递增的 id,eject(id) 移除指定处理器,clear() 清空整个栈;options 还支持 synchronous 与 runWhen 两个选项,与类型声明中的 AxiosInterceptorOptions 一一对应。
为错误标注类型(Typing errors)
使用 axios.isAxiosError() 类型守卫收窄捕获到的错误:
import axios, { AxiosError } from "axios";
type ApiError = {
message: string;
code: number;
};
try {
await axios.get("/api/protected-resource");
} catch (error) {
if (axios.isAxiosError<ApiError>(error)) {
// error.response?.data is typed as ApiError
console.error(error.response?.data.message);
console.error(error.response?.status);
} else {
throw error;
}
}
isAxiosError 在类型层面的签名是标准守卫(见 index.d.ts):
export function isAxiosError<T = any, D = any, P = any>(
payload: any
): payload is AxiosError<T, D, P>;
传入业务错误类型 ApiError 作为 T 后,收窄得到的 AxiosError<ApiError> 中 response?.data 即为 ApiError,response?.status 为 number | undefined。运行时对应的实现挂载在默认导出上(axios.isAxiosError = isAxiosError,见 lib/axios.js),检查逻辑见 lib/helpers/isAxiosError.js。
AxiosError 类本身携带丰富的类型化静态错误码,便于按 error.code 精确分支处理(见 index.d.ts):
static readonly ERR_NETWORK = 'ERR_NETWORK';
static readonly ERR_BAD_RESPONSE = 'ERR_BAD_RESPONSE';
static readonly ERR_CANCELED = 'ERR_CANCELED';
static readonly ECONNABORTED = 'ECONNABORTED';
static readonly ETIMEDOUT = 'ETIMEDOUT';
// ...
此外,取消错误可以用配套的 axios.isCancel<T>() 守卫收窄为 CanceledError<T>(类型声明见 index.d.ts 与 index.d.ts),配合 AbortController 使用,更多模式可参考 TypeScript 文档页。
TypeScript 编译器配置注意事项
由于 axios 以 ESM 为默认导出、CJS 为 module.exports 双格式发布,不同 tsconfig 下存在一些注意点(与 TypeScript 文档页 中 “Module resolution caveats” 的说明一致):
- 推荐
"moduleResolution": "node16"(由"module": "node16"隐含),要求 TypeScript 4.7 及以上。这是与index.d.ts头部TypeScript Version: 4.7声明匹配的配置。 - 若使用 ESM 编译,默认设置通常即可。
- 若编译目标为 CJS 且无法使用
"moduleResolution": "node16",必须启用"esModuleInterop": true。这对应 CJS 端axios.default = axios的兜底设计(lib/axios.js):开启esModuleInterop后,import axios from "axios"在 CJS 编译下能正确取到module.exports.default上的完整对象。 - 若使用 TypeScript 对 CJS JavaScript 代码做类型检查(
checkJs),唯一选择是"moduleResolution": "node16"。
这些并非纸面约定:仓库自带针对两种模块格式的类型兼容性测试。CJS 侧的 tests/module/cjs/tests/typings.module.test.cjs 与 ESM 侧的 tests/module/esm/tests/typings.module.test.js 都创建临时工程并以如下配置运行 tsc --noEmit:
const tsconfig = {
compilerOptions: {
checkJs: true,
module: 'node16',
},
};
其中 CJS 侧还会专门验证 isCancel 收窄到 CanceledError 的类型行为(cjs-is-cancel-typing.ts 夹具),与上文“为错误标注类型”一节的做法互为印证。
小结
- axios 通过
index.d.ts+index.d.cts双类型文件配合exports映射,为 ESM/CJS 两种消费方式提供开箱即用的类型支持,最低 TypeScript 4.7; - 请求方法统一采用
<T, R, D, P>泛型模式,axios.get<Post>(...)即可精确标注response.data;POST 场景可同时约束请求体与响应; axios.create返回可调用、可链式创建的AxiosInstance,baseURL/timeout/headers 从创建起即受类型保护;- 请求拦截器必须标注
InternalAxiosRequestConfig(headers必填且为AxiosHeaders实例),响应拦截器标注AxiosResponse; axios.isAxiosError<T>()与axios.isCancel<T>()是catch块中收窄错误类型的标准手段,配合AxiosError的静态错误码可做精确分支处理;- tsconfig 首选
"moduleResolution": "node16";无法使用时编译 CJS 需开启esModuleInterop。
主要参考文件:index.d.ts、index.d.cts、package.json、lib/axios.js、lib/core/InterceptorManager.js、TypeScript 文档页、tests/module/cjs 与 tests/module/esm 类型测试。
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 StartedRust0624
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