typescript实现原理-TypeScript套接字参数/响应类型手动推断

TypeScript Web 项目的 API 参数和响应数据类型如果没有自动映射,默认是不存在的:

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

这给项目带来了一点不稳定。 如果复杂的话,每个socket的响应数据任意,各种socket/返回数据相互依赖,混乱程度可想而知。

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

指定响应类型

查看axios的类型可以看出,它支持socket响应类型的开发:

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

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

// 假定接口A的路径是 '/apple', 响应类型是 AppleResinterface AppleRes {  code: number  data: string}async function sendRequest(url: string, params?: any) {  const response = axios.get(url, { params })  return response}const apple = sendRequest('/apple') // -> Promise<AxiosResponse>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会提示该属性不存在。

但到目前为止似乎并没有多大帮助。 毕竟我们也可以编译为AppleRes映射类型,下面继续。

指定参数类型

映射参数类型很简单,只需要在params参数中指定即可:

// 假定接口A的路径是 '/apple', 参数类型是 AppleReq, 响应类型是 AppleResinterface AppleReq {  pageNum: number  pageSize?: number}async function sendRequest(url: string, params?: R) {  const response = axios.get(url, { params })  return response}const apple = sendRequest('/apple', {	pageNum: 1, // -> number 	blah: 1 // Error: Argument of type '{ pageNum: number; blah: number; }' is not assignable to parameter of type 'AppleReq'.})复制代码

这样,如果我们输入了错误的参数,TS也可以纠正它。

然而,这似乎还不够。 这样的话每次请求socket的时候都自动输入Req和Res的类型就很麻烦了。

有没有办法让TS在输入sendRequest('/apple')请求路径时推断出请求和响应数据的类型?

绑定请求路径&参数&响应数据类型

假设我们有很多socket,我们一一定义它们的映射关系typescript实现原理,使用接口是相当合适的:

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 }}复制代码

很多公司都有内部的socket管理平台,YY的就是Tagee平台。 还有社区版本,比如Swagger、Rap2等,以上部分可以通过socket管理平台轻松批量生成。

在这种情况下,我们可以通过键“/apple”获取该路径的请求和响应类型:

type AppleApiMap = ApiMaps['/apple']// 等价于:type AppleApiMap = {    req: AppleReq;    res: AppleRes;}复制代码

然后,我们在sendRequest中映射:

// 获得请求路径的类型集合:type ApiKeys = keyof ApiMaps async function sendRequest(url: T, params?: ApiMaps[T]['req']) {  const response = await axios.get(url, { params })  return response}复制代码

说明:T extends ApiKeys = ApiKeys 表示上述类库T是ApiKeys集合之一typescript实现原理,即'/apple'、'/banana'和'/cat'之一。

= ApiKeys 是基类的默认值。 如果我们不传入子类参数,TS可以使用实际传入参数的类型作为默认类型。 请参阅:TypeScript:文档 - 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中,会手动提示路径,类型可选: