在ts中为axios添加参数约束和返回值推导
前言
随着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 })
传入的参数不对。
这就足够了吗? 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);
}
这样一来,我们写配置的时候就会有智能提示了,并且书写错误的时候也会有警告。
以下是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);
}
我们定义一个 IResponseData
为axios
的数据类型,其中 data
字段约束为 IPlusReturnValue
。它继承自IPlusParams
,然后我们将instance.request
的泛型参数约束为IResponseData
。这样一来,编译器就能自动推断出 .then((res) => res.data.data);
中res.data
的类型为IResponseData
,res.data.data
的类型为IPlusReturnValue
由于函数能自动推导返回值,所以我们的request
函数和testApi
函数现在能自动推导出返回类型:
也就是说,testApi
现在能推导出返回值为Promise<IPlusReturnValue>
了。我们在调用testApi
函数调时,便可以推导出res
形参是IPlusReturnValue
类型。
同时,我们此前出现的拼写错误的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[]
。
到这一步我们就编写了一个更加通用版本的 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的过程中的一点小总结,如果本文有什么错误的地方,欢迎大家指正,如果觉得有帮助的话,记得点个✨ 👍 ✨,谢谢各位!
转载自:https://juejin.cn/post/7045110585188417550