首页
/ axios TypeScript 类型系统实战:泛型请求、类型化实例、类型守卫与 ESM/CJS 模块解析配置

axios TypeScript 类型系统实战:泛型请求、类型化实例、类型守卫与 ESM/CJS 模块解析配置

2026-09-06 17:54:57作者:龚格成

axios 原生内置 TypeScript 类型定义,无需依赖 @types/axios 第三方包。本文以 axios 仓库中“TypeScript example”文档为主线,完整覆盖类型导入、泛型请求标注、函数/POST 请求类型封装、类型化实例与拦截器、错误类型收窄等核心操作,并结合 index.d.tspackage.jsonlib/ 源码剖析背后的类型体系与双格式(ESM/CJS)分发机制,帮助你在 TypeScript 项目中获得端到端的类型安全。

原生内置类型:无需安装 @types 包

axios 的类型定义随 npm 包一起发布,通过 index.d.ts(ESM 入口)和 index.d.cts(CJS 入口)两套声明文件分别服务两种模块格式。从 package.jsonexports 字段可以看到分发规则:

"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 响应对象,Tresponse.data 的类型
AxiosError<T, D, P> index.d.ts 错误对象,携带 responseconfigcode 等属性
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)四个泛型的职责分别是:

  • Tresponse.data 的类型,即上文示例中的 Post
  • R:自定义整个响应形状,默认 AxiosResponseDefault(唯一符号标记,解析为标准的 AxiosResponse<T, D, {}, P>);
  • D:请求体类型;
  • P:查询参数 params 的类型,默认 any

因此 axios.get<Post>(...) 等价于声明 Promise<AxiosResponse<Post, any, {}, any>>response.data 被精确推断为 Postresponse.data.titlestring 而非 anydeleteheadoptionspostputpatchpostForm 等所有请求方法都遵循相同的 <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.dataPost,函数签名 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, ...>——因为 AxiosRequestConfigdata?: D 字段与请求方法共享 D 这个类型槽位(见 index.d.ts)。此外,paramsSerializer 也会接收与 P 一致的参数类型,可参考仓库英文文档 TypeScript 页 中 “Typing request data and query params” 一节的完整示例。

类型化的 axios 实例(Typed axios instance)

创建类型化实例,可以把 baseURLtimeout、默认 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.jsaxios.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 实例,setgetnormalize 等方法(见 index.d.ts 中的 AxiosHeaders 类声明)才能被正确调用。

拦截器的运行时行为可在 lib/core/InterceptorManager.js 中印证:use(fulfilled, rejected, options) 注册处理器并返回递增的 ideject(id) 移除指定处理器,clear() 清空整个栈;options 还支持 synchronousrunWhen 两个选项,与类型声明中的 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 即为 ApiErrorresponse?.statusnumber | 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.tsindex.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 返回可调用、可链式创建的 AxiosInstancebaseURL/timeout/headers 从创建起即受类型保护;
  • 请求拦截器必须标注 InternalAxiosRequestConfigheaders 必填且为 AxiosHeaders 实例),响应拦截器标注 AxiosResponse
  • axios.isAxiosError<T>()axios.isCancel<T>()catch 块中收窄错误类型的标准手段,配合 AxiosError 的静态错误码可做精确分支处理;
  • tsconfig 首选 "moduleResolution": "node16";无法使用时编译 CJS 需开启 esModuleInterop

主要参考文件:index.d.tsindex.d.ctspackage.jsonlib/axios.jslib/core/InterceptorManager.jsTypeScript 文档页tests/module/cjstests/module/esm 类型测试。

登录后查看全文
热门项目推荐
相关项目推荐