TypeScript Web 项目的API 的参数与响应数据类型,如果不手动映射,默认是缺失的:
代码语言:javascript复制async function sendRequest(url: string, params?: any) {
const response = axios.get(url, { params })
return response // -> Promise<AxiosResponse<any, any>>
}
复制代码
这给项目带来了少许不稳定性。如果复杂的话,每个接口的响应数据都是 any
,各种接口/返回数据互相依赖,可想其混乱程度。
以下通过编写一个通用的请求函数 sendRequest
来实现(跳转实际效果示例):
指定响应类型
查看 axios 的类型,可知是支持制定接口响应类型的:
代码语言:javascript复制export class Axios {
get<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
}
复制代码
具体做法是指定泛型 T参数,来让 TS 推导出响应数据类型,修改初始代码:
代码语言:javascript复制// 假定接口A的路径是 '/apple', 响应类型是 AppleRes
interface AppleRes {
code: number
data: string
}
async function sendRequest<T>(url: string, params?: any) {
const response = axios.get<T>(url, { params })
return response
}
const apple = sendRequest<AppleRes>('/apple') // -> Promise<AxiosResponse<AppleRes, any>>
apple.then((res) => {
const blah = res.data.data // -> string
const blah2 = res.data.data2 // Error: Property 'data2' does not exist on type 'AppleRes'. Did you mean 'data'?
})
复制代码
这时候TS能够推导响应类型了, 当我们输入不存在的属性的时候,TS提示属性不存在。
可是至此好像并没有很大的帮助,毕竟我们也可以在请求编写 as AppleRes
映射类型,下面继续。
指定参数类型
映射参数类型是简单的, 只需要在 params 参数指定:
代码语言:javascript复制// 假定接口A的路径是 '/apple', 参数类型是 AppleReq, 响应类型是 AppleRes
interface AppleReq {
pageNum: number
pageSize?: number
}
async function sendRequest<T, R>(url: string, params?: R) {
const response = axios.get<T>(url, { params })
return response
}
const apple = sendRequest<AppleRes, AppleReq>('/apple', {
pageNum: 1, // -> number
blah: 1 // Error: Argument of type '{ pageNum: number; blah: number; }' is not assignable to parameter of type 'AppleReq'.
})
复制代码
这样,如果我们输入错了参数,TS也能够纠正。
可是,貌似还是不够。这样的话,每次请求接口都需要手动输入 Req, Res 的类型,很麻烦。
有没有一个方法可以输入 sendRequest('/apple')
请求路径的时候, 就能够让 TS 推导请求&响应数据的类型呢?
绑定请求路径&参数&响应数据类型
假定我们有很多个接口,我们一一定义它们的映射关系,使用 interface 挺合适:
代码语言:javascript复制interface AppleRes {
code: number
data: string
}
interface AppleReq {
pageNum: number
}
interface BananaRes {
code: number
data: object
}
interface BananaReq {
pageSize: number
}
//...
// 关键: 在 ApiMaps 绑定它们的映射关系
interface ApiMaps {
'/apple': { req: AppleReq; res: AppleRes }
'/banana': { req: BananaReq; res: BananaRes }
'/cat': { req: CatReq; res: CatRes }
}
复制代码
很多企业都有内部的接口管理平台,YY的是 Tagee 平台。社区版本也有,如 Swagger,Rap2等,以上部分可以通过接口管理平台轻松批量生成。
这样的话我们可以通过 '/apple'
这个键来获得这个路径的请求和响应类型:
type AppleApiMap = ApiMaps['/apple']
// 等价于:
type AppleApiMap = {
req: AppleReq;
res: AppleRes;
}
复制代码
然后,我们在 sendRequest
映射:
// 获得请求路径的类型集合:
type ApiKeys = keyof ApiMaps
async function sendRequest<T extends ApiKeys = ApiKeys>(url: T, params?: ApiMaps[T]['req']) {
const response = await axios.get<ApiMaps[T]['res']>(url, { params })
return response
}
复制代码
说明: T extends ApiKeys = ApiKeys
表示以上泛型 T 是 ApiKeys
集合中的一个,即 '/apple'
, '/banana'
, '/cat'
其一。
= ApiKeys
则是泛型默认值,如果我们没有传入泛型参数时候,TS可以使用实际传入参数的类型作为默认类型。可参考:TypeScript: Documentation - TypeScript 2.3 (typescriptlang.org)
实际效果
代码语言:javascript复制const apple = sendRequest('/apple', { pageNum: 1 })
apple.then((res) => {
const blah = res.data.data // -> string
const blah2 = res.data.data2 // Error: Property 'data2' does not exist on type 'AppleRes'. Did you mean 'data'?
})
const banana = sendRequest('/banana', { pageSize: 1 })
banana.then((res) => {
const blah = res.data.data // -> boolean
})
复制代码
在 VSCode中还会自动提示有什么路径,类型可选: