在ts中为axios添加参数约束和返回值推导

6,566 阅读2分钟

前言

随着vue3的不断推行,越来越多的小伙伴开始加入typescript的怀抱中,开始享受类型化带来的好处。 axios 是常见的ajax库,然而我发现很多小伙伴在使用它的时候打开姿势不太对,导致并没有享受到类型化带来的好处,例如参数字段校验,返回类型自动推导等。 如果你也正在被这些问题困扰的话,那就请继续阅读下去吧。

本文并不会讨论拦截器的相关知识,因为那不是本文的重点。

作者只是一个菜鸟,文中如有错误的地方,欢迎大家指正。

需求

假设我们有一个后端api接口,它就是一个简单的加法接口,接受两个加数,返回一个包含加数以及结果的一个json对象

请求格式
url: '/api/test-plus'
methods: 'post'
data: {a:2,b:3}

请求成功的返回格式是一个json数据: 
{
  code:0,
  message:"",
  data:{
     a:2,
     b:3,
     result:5
  }
}

我们后续的封装都是为了让我们在请求这个接口的时候,代码能推断出返回的结构。

实现参数约束和返回值推导

首先为我们的ts工程参加一个./src/demo.ts文件,并且写一份不带类型参数的代码:

import axios from "axios";

// 创建一个axios实例
const instance = axios.create();

// 通用的请求函数
export async function request(config: any) {
  return instance.request(config).then((res) => res.data.data);
}

export async function testApi(data: any) {
  return request({
    url: "/api/test-plus",
    method: "POST",
    data: data,
  });
}

// 调用这个接口
testApi({ a: 2 }).then((res) => {
  console.log(`${res.a} + ${res.b} = ${res.resut}`);
});

乍一看,这段代码似乎没有什么问题,但细细看的话,你会发现有两个不足之处:

  • 在最后一行我们调用接口的时候,本来应该传{a:2,b:3},但是我们只传了{a:2},在传给后端以后会报错。
  • 在打印结果的时候,res.resut拼写错误,到运行时的时候会出现 undefined。 细想之下,由于我们没有约定testApi的参数,所以理论上我们无论传什么给testApi都行,同时.then(res) 中,res推断出来的是any类型,导致我们在写形如 res. 这样的代码的时候,编辑器也并不会给我们提示,这样的ts代码简直让人无法接受。

那么我们要该如何约束他们呢?

首先我们约束一下参数:

interface IPlusParams {
  a: number;
  b: number;
}

export async function testApi(data: IPlusParams) {
  return request({
    url: "/api/test-plus",
    method: "POST",
    data: data,
  });
}

我们添加了IPlusParams接口,并形参data,这样依赖,typescript就会抱怨调用testApi的时候testApi({ a: 2 })传入的参数不对。

image.png

这就足够了吗? no no no! 我们不仅想约束data的类型,同时我们希望在写

request({
    url: "/api/test-plus",
    method: "POST",
    data: data,
});

这样的代码的时候,我们的编辑器能自动提示,比如我们敲下u,编辑器就给我们提示url。那么要怎么做呢? 很简单,我们为 request函数的参数config添加约束即可。

import axios, { AxiosRequestConfig } from "axios";

// 通用的请求函数
export async function request(config: AxiosRequestConfig) {
  return instance.request(config).then((res) => res.data.data);
}

这样一来,我们写配置的时候就会有智能提示了,并且书写错误的时候也会有警告。 image.png image.png

以下是AxiosRequestConfig定义的参考

export interface AxiosRequestConfig<D = any> {
  url?: string;
  method?: Method;
  baseURL?: string;
  transformRequest?: AxiosRequestTransformer | AxiosRequestTransformer[];
  transformResponse?: AxiosResponseTransformer | AxiosResponseTransformer[];
  headers?: AxiosRequestHeaders;
  params?: any;
  paramsSerializer?: (params: any) => string;
  data?: D;
  timeout?: number;
  timeoutErrorMessage?: string;
  withCredentials?: boolean;
  adapter?: AxiosAdapter;
  auth?: AxiosBasicCredentials;
  responseType?: ResponseType;
  xsrfCookieName?: string;
  xsrfHeaderName?: string;
  onUploadProgress?: (progressEvent: any) => void;
  onDownloadProgress?: (progressEvent: any) => void;
  maxContentLength?: number;
  validateStatus?: ((status: number) => boolean) | null;
  maxBodyLength?: number;
  maxRedirects?: number;
  socketPath?: string | null;
  httpAgent?: any;
  httpsAgent?: any;
  proxy?: AxiosProxyConfig | false;
  cancelToken?: CancelToken;
  decompress?: boolean;
  transitional?: TransitionalOptions;
  signal?: AbortSignal;
  insecureHTTPParser?: boolean;
}

接下来,我们约束返回值。

interface IPlusParams {
  a: number;
  b: number;
}

interface IPlusReturnValue extends IPlusParams {
  result: number;
}

export interface IResponseData {
  code: number;
  message: string;
  data: IPlusReturnValue;
}

// 通用的请求函数
export async function request(config: AxiosRequestConfig) {
  return instance.request<ResponseData>(config).then((res) => res.data.data);
}

我们定义一个 IResponseDataaxios的数据类型,其中 data字段约束为 IPlusReturnValue。它继承自IPlusParams,然后我们将instance.request的泛型参数约束为IResponseData。这样一来,编译器就能自动推断出 .then((res) => res.data.data);res.data的类型为IResponseDatares.data.data 的类型为IPlusReturnValue

image.png

image.png

由于函数能自动推导返回值,所以我们的request函数和testApi函数现在能自动推导出返回类型:

image.png

image.png

也就是说,testApi现在能推导出返回值为Promise<IPlusReturnValue>了。我们在调用testApi函数调时,便可以推导出res形参是IPlusReturnValue类型。

image.png

同时,我们此前出现的拼写错误的bug编译器也给我们识别出来了。到这里我们的已经达成了我们预定的小目标。

编写更通用的request 函数

在上面的代码中我们已经能正确地推断出testApi的参数类型和返回类型了,但是还有改进的地方吗? 答案是有。

export interface IResponseData {
  code: number;
  message: string;
  data: IPlusReturnValue;
}

目前为止,我们的IResponseData 中的data是写死的IPlusReturnValue类型,这显然是不合理的,我们需要一个更普遍的接口,所以这里我们引入泛型。

export interface IResponseData<T = any> {
  code: number;
  message: string;
  data: T;
}

// 改造为泛型接口
export async function request<T>(config: AxiosRequestConfig) {
  return instance
    .request<IResponseData<T>>(config)  // 这里是重点
    .then((res) => res.data.data);
}

// 显式传入泛型参数 IPlusReturnValue
export async function testApi(data: IPlusParams) {
  return request<IPlusReturnValue>({ 
    url: "/api/test-plus",
    method: "POST",
    data: data,
  });
}

我们将IResponseData接口改造成泛型接口,将data的类型改为动态传入,然后将request函数也改造泛型函数,然后在testApi内部,显式传入IPlusReturnValue。这样一来就实现了request函数的通用性改造。

现在,假如我们有一个新的接口,返回一个随机字符串数组

请求格式
url: '/api/test-list'
methods: 'get'

请求成功的返回格式是一个json数据,其中data是字符串数组: 
{
  code:0,
  message:"",
  data:["xxx"]
}

那么我们只需新增一个接口函数

export async function testApi2() {
  return request<string[]>({
    url: "/api/test-list",
    method: "GET",
  });
}

testApi2().then((list) => {
  list.forEach((a) => console.log(a));
});

编译器能正常地推测出.then(list)list的类型是string[]

image.png

到这一步我们就编写了一个更加通用版本的 request 函数。

完整代码

import axios, { AxiosRequestConfig } from "axios";

// 创建一个axios实例
const instance = axios.create();

// 泛型接口,T的类型支持
export interface IResponseData<T = any> {
  code: number;
  message: string;
  data: T;
}

// 通用的请求函数
export async function request<T>(config: AxiosRequestConfig) {
  return instance
    .request<IResponseData<T>>(config)
    .then((res) => res.data.data);
}

interface IPlusParams {
  a: number;
  b: number;
}
interface IPlusReturnValue extends IPlusParams {
  result: number;
}
export async function testApi(data: IPlusParams) {
  return request<IPlusReturnValue>({
    url: "/api/test-plus",
    method: "POST",
    data: data,
  });
}

// 调用这个接口
testApi({ a: 2, b: 3 }).then((res) => {
  console.log(`${res.a} + ${res.b} = ${res.result}`);
});

export async function testApi2() {
  return request<string[]>({
    url: "/api/test-list",
    method: "GET",
  });
}

testApi2().then((list) => {
  list.forEach((a) => console.log(a));
});

总结

本文通过一个小案例,为封装了一个request函数,使得我们在使用axios能享受到类型编程带来的福利,主要的方法就是利用泛型机制,增加函数的通用性。

作者是一个正在学习ts的菜鸟,本文是我在学习ts的过程中的一点小总结,如果本文有什么错误的地方,欢迎大家指正,如果觉得有帮助的话,记得点个✨ 👍 ✨,谢谢各位!