zl程序教程

您现在的位置是:首页 >  其他

当前栏目

TypeScript接口参数/响应类型自动推导

2023-04-18 14:44:42 时间

TypeScript Web 项目的API 的参数与响应数据类型,如果不手动映射,默认是缺失的:

async function sendRequest(url: string, params?: any) {
  const response = axios.get(url, { params })
  return response  // -> Promise<AxiosResponse<any, any>> 
}
复制代码

这给项目带来了少许不稳定性。如果复杂的话,每个接口的响应数据都是 any,各种接口/返回数据互相依赖,可想其混乱程度。

以下通过编写一个通用的请求函数 sendRequest 来实现(跳转实际效果示例):

指定响应类型

查看 axios 的类型,可知是支持制定接口响应类型的:

export class Axios {
    get<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
}
复制代码

具体做法是指定泛型 T参数,来让 TS 推导出响应数据类型,修改初始代码:

// 假定接口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 参数指定:

// 假定接口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 挺合适:

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 平台。社区版本也有,如 SwaggerRap2等,以上部分可以通过接口管理平台轻松批量生成。

这样的话我们可以通过 '/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)

实际效果

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中还会自动提示有什么路径,类型可选: