axios 1.x TypeScript 实战指南:类型导入、请求泛型、拦截器配置与错误类型收窄
【免费下载链接】axiosPromise based HTTP client for the browser and node.js项目地址: https://gitcode.com/GitHub_Trending/ax/axios
本文以 axios 仓库中官方的 TypeScript 入门示例文档(docs/fr/pages/getting-started/examples/typescript.md,英文原版见 docs/pages/getting-started/examples/typescript.md)为主体,系统讲解在 axios 1.x 中如何导入类型、用泛型约束请求与响应、创建带类型实例、编写带类型的拦截器并收窄错误类型。读完本篇,你将掌握 axios 与 TypeScript 集成的完整用法,并能对照 index.d.ts 源码理解每个 API 背后的类型签名设计。
类型从哪里来:随包发布的类型定义
axios 开箱即用地附带 TypeScript 类型定义,无需额外安装@types/axios。类型入口就是仓库根目录下的 index.d.ts(ESM 类型,787 行)与 index.d.cts(CJS 类型),二者由package.json的exports映射在模块解析时自动选择:
"types": "index.d.ts", "exports": { ".": { "types": { "require": "./index.d.cts", "default": "./index.d.ts" }, "default": { "require": "./dist/node/axios.cjs", "default": "./index.js" } } }也就是说,axios 同时双份发布 ESM 与 CJS 产物:ESM 入口是 index.js,CJS 入口是dist/node/axios.cjs,类型文件也随之分成了index.d.ts和index.d.cts两份。这也解释了后文「TypeScript 配置注意事项」一节中为什么不同moduleResolution配置下表现会有差异。
类型定义的最低 TypeScript 版本要求直接写在 index.d.ts#L1 的第一行:
// TypeScript Version: 4.7因此 axios 的类型系统要求TypeScript 4.7 或更高版本,这与后文推荐"moduleResolution": "node16"(由 TS 4.7 引入)的结论是一致的。
导入类型
axios 的类型可以直接从"axios"模块命名导入。仓库根目录的 index.js 会把默认导出的 axios 实例「解包」为与静态属性一致的命名导出(create、Axios、AxiosError、isAxiosError、AxiosHeaders等),因此在 TypeScript 侧同样可以按命名方式引用类型:
import axios from "axios"; import type { AxiosRequestConfig, AxiosResponse, AxiosError } from "axios";建议在只需要类型信息时使用import type,这样编译器会在编译期将其完全擦除,不产生任何运行时开销。这几个基础类型在 index.d.ts 中的定义位置分别是:AxiosRequestConfig(index.d.ts#L391-L480)、AxiosResponse(index.d.ts#L515-L522)、AxiosError(index.d.ts#L524-L564)。
给请求打上类型:响应泛型
使用响应类型的泛型参数,即可告知 TypeScript 你的数据将呈现什么形状。以下示例沿用官方文档,以 jsonplaceholder 的 posts 接口为例:
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泛型参数的底层签名
从 index.d.ts#L654-L708 可以看到,Axios类上每个请求方法都接受四个泛型参数:
get<T = any, R = AxiosResponseDefault, D = any, P = any>( url: string, config?: AxiosRequestConfig<D, P> ): Promise<AxiosResponseResult<T, R, D, P>>;T:响应data的类型,即上面例子中传入的Post;R:响应对象本身的结果形状,默认为AxiosResponse(即Promiseresolve 出response.data、response.status等标准结构);D:请求体的类型,最终体现在config.data?: D上(见 index.d.ts#L403);P:查询参数的类型,体现在config.params?: P上(见 index.d.ts#L399)。
AxiosResponse接口本身也带有泛型(index.d.ts#L515-L522):
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; }因此axios.get<Post>(...)返回的实际上是Promise<AxiosResponse<Post, any, {}, any>>,response.data被精确推断为Post,.title自然就是string。
给函数打上类型
将请求封装进带显式返回类型的函数,可以获得最大的类型安全:
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; };在 lib/core/Axios.js#L268-L306 中可以看到,get、post等便捷方法在运行时都是对核心request()方法的别名封装(无参方法如delete/get/head/options走一个分支,带数据的方法post/put/patch/query走另一个分支,并额外生成postForm等 Form 变体)。类型定义则为这些别名逐一声明了与request相同的四泛型签名,所以无论用axios.get<Post>(url)还是axios.request<Post>({ url }),类型行为完全一致。
给 POST 请求打类型
POST 场景可以同时约束请求体和期望的响应。注意post的泛型参数顺序是post<T, R, D, P>——第一个泛型仍是响应T,请求体类型是第三个D:
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; };对照签名(index.d.ts#L673-L677):
post<T = any, R = AxiosResponseDefault, D = any, P = any>( url: string, data?: D, config?: AxiosRequestConfig<D, P> ): Promise<AxiosResponseResult<T, R, D, P>>;如果还想连请求体一起约束,可以写成axios.post<CreatePostResponse, AxiosResponse<CreatePostResponse>, CreatePostBody>(url, data),此时data参数会被推断为CreatePostBody,传错字段会直接编译报错。
带类型的 axios 实例
创建类型化实例,把 base URL 与默认头固定在其中:
import axios from "axios"; import type { AxiosInstance } from "axios"; const api: AxiosInstance = axios.create({ baseURL: "https://api.example.com", timeout: 5000, });axios.create的返回类型在 index.d.ts#L719 中声明为AxiosInstance,其参数类型是CreateAxiosDefaults(index.d.ts#L508-L513),即Omit<AxiosRequestConfig, 'headers'>加上一个更宽松的头类型RawAxiosRequestHeaders | AxiosHeaders | Partial<HeadersDefaults>——因为create时传入的只是「默认值原料」,真正的AxiosHeaders实例会在实例化之后由内部构造。
从 lib/axios.js#L28-L47 的createInstance实现看,axios.create(config)会new Axios(defaultConfig)、把原型方法与实例状态合并绑定,并挂上instance.create工厂方法(内部通过mergeConfig(defaultConfig, instanceConfig)继承父实例配置)。默认导出的axios本身就是用defaults创建的这样一个实例。
AxiosInstance还定义了两个可调用重载(index.d.ts#L710-L717),所以除了api.get(...)之外,还可以像 fetch 一样直接以配置对象或 URL 字符串调用:
api({ url: "/posts", method: "get" }); api("/posts", { params: { id: 1 } });带类型的拦截器
在 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) );(示例中api即上一小节创建的实例。)
为什么必须是 InternalAxiosRequestConfig
两者的定义差异只有两行(index.d.ts#L485-L487):
export interface InternalAxiosRequestConfig<D = any, P = any> extends AxiosRequestConfig<D, P> { headers: AxiosRequestHeaders; }AxiosRequestConfig.headers是可选的、类型较宽的联合类型(RawAxiosRequestHeaders & MethodsHeaders或AxiosHeaders);而InternalAxiosRequestConfig.headers是必填的AxiosRequestHeaders(RawAxiosRequestHeaders & AxiosHeaders,index.d.ts#L130)。这对应了运行时事实:请求进入拦截器链之前,config.headers一定已被构造为AxiosHeaders实例——见 lib/core/Axios.js#L153-L164,在_request中 headers 会先按方法合并再执行config.headers = AxiosHeaders.concat(contextHeaders, headers)。
正因如此,示例里的config.headers.set("Authorization", ...)才能通过类型检查:set方法是AxiosHeaders类成员(index.d.ts#L30-L36),而不是普通对象字面量上的属性。
拦截器的管理器接口为AxiosInterceptorManager<V>(index.d.ts#L639-L644),请求拦截器use的第三个可选参数类型是AxiosInterceptorOptions:
export interface AxiosInterceptorOptions { synchronous?: boolean; runWhen?: ((config: InternalAxiosRequestConfig) => boolean) | null; }运行时实现见 lib/core/InterceptorManager.js#L67-L92:use(fulfilled, rejected, options)返回一个数字id,可用eject(id)移除、clear()清空。所以给拦截器注册留一个变量是常见写法:
const id = api.interceptors.request.use( (config) => { config.headers.set("Authorization", `Bearer ${getToken()}`); return config; }, (error) => Promise.reject(error) ); // 不再需要时:api.interceptors.request.eject(id);此外注意AxiosRequestInterceptorUse的签名是(value: T) => T | Promise<T>(index.d.ts#L618-L625),请求拦截器既允许同步返回config,也允许返回Promise<InternalAxiosRequestConfig>。
给错误打上类型
捕获错误时,使用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 被推断为 ApiError console.error(error.response?.data.message); console.error(error.response?.status); } else { throw error; } }isAxiosError在类型层面是一个类型守卫(index.d.ts#L749-L751):
export function isAxiosError<T = any, D = any, P = any>( payload: any ): payload is AxiosError<T, D, P>;泛型T透传给AxiosError<T>,而AxiosError.response?: AxiosResponse<T, D, {}, P>(index.d.ts#L536),所以传入ApiError后,error.response?.data就被精确收窄为ApiError,.message可直接访问。
运行时判断逻辑非常轻量,见 lib/helpers/isAxiosError.js#L12-L14:
export default function isAxiosError(payload) { return utils.isObject(payload) && payload.isAxiosError === true; }它只检查对象上是否带有isAxiosError === true标记(AxiosError构造时写入)。AxiosError还暴露了一组静态错误码常量(index.d.ts#L550-L563),如ERR_NETWORK、ERR_CANCELED、ETIMEDOUT,配合error.code字段可用于在catch分支中区分网络故障、取消与超时。
TypeScript 配置注意事项
由于 axios 同时发布 ESM 与 CJS 两个版本,tsconfig.json中有一些需要注意的细节:
- 推荐配置是
"moduleResolution": "node16"(由"module": "node16"隐含),需要 TypeScript 4.7 或更高版本。只有该解析模式才会读取package.json中的exports映射,从而按require/import两种场景正确选中index.d.cts或index.d.ts。axios 仓库自己的 tsconfig.json 也正是使用"module": "node16"+"strict": true; - 如果你把 TypeScript 编译为 CJS、又无法使用
"moduleResolution": "node16",请开启"esModuleInterop": true,以兼容 axios 默认导出与 CJS 互操作; - 如果用 TypeScript 来给 CJS 的 JavaScript 代码做类型检查,唯一可行的选项是
"moduleResolution": "node16"。
延伸阅读
- 类型定义全文:index.d.ts(ESM)、index.d.cts(CJS)
- 核心类与默认实例:lib/core/Axios.js、lib/axios.js
- 拦截器管理器实现:lib/core/InterceptorManager.js
- 更深入的 TypeScript 主题(module augmentation 扩展
AxiosRequestConfig自定义符号属性、D/P泛型在response.config上的保留等),可继续阅读 docs/pages/advanced/type-script.md - 类型层面的回归测试位于 tests/module/cjs/tests/cjs-typing.ts 与 tests/module/esm/tests/typings.module.test.js,可作为各模块系统下类型行为的验证参考
【免费下载链接】axiosPromise based HTTP client for the browser and node.js项目地址: https://gitcode.com/GitHub_Trending/ax/axios
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考