如何给你的接口返回值加上类型提示

892 阅读3分钟

如何给你的接口返回值加上类型提示

屏幕录制2023-04-25 15.29.48.gif

如今随着 typescript 的流行,我们在开发过程中得到了很多收益,前端很多项目在做技术选型时对库的选择有很个很重要的指标就是这个库有没有ts类型定义,好的类型定义能极大的降低上手一个新库的学习成本。

同样,相信前端的朋友们在对接后端接口时也希望能有个实时的接口类型提示,比如

axiosInstance.get('/pets/{id}').then(res => {
  res.data // data type is Pet
})

其中,获取 /v1/some/url 的资源时,res的类型为AxiosResponse类型, 这个类型大致包含如下字段

interface AxiosResponse<T = any, D = any> {
  data: T;
  status: number;
  statusText: string;
  headers: RawAxiosResponseHeaders | AxiosResponseHeaders;
  config: InternalAxiosRequestConfig<D>;
  request?: any;
}

在实际业务中,data的类型通常需要手动定义,然后在get时指定这个类型

interface Pet {
  /**
  * 宠物名
  */
  name: string;
  id: string;
  /**
  * 宠物标签
  */
  tag?: string;
}
axiosInstance.get<Dog>('/pets/{id}').then(res => {
    res.data.name // data type is Pet
})

这样虽然可以达到目的,但使用起来还是不太方便,如果可以在给定url时,res.data 自动映射成Dog类型,不是可以省了手动定义的麻烦吗?

如果要根据url自动映射类型,那么Dog类型要从哪里获取呢?其实openapi 已经替我们解决了这个问题。

在项目接口对接的过程中,后端通常会提供一个 swagger ui 页面,而这个页面的数据是根据一个固定格式的json 文件渲染出来的,这个文件的内容大致如下

// type-define.json

{
  "openapi": "3.0.0",
  "info": {
    //... etc
  },
  "servers": [ //... etc 
  ],
  "paths": {
    "/pets": {
      "get": {
       
        "parameters": [
          // ...etc
        ],
        "responses": {
          "200": {
            "description": "pet response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Pet"
                  }
                }
              }
            }
          },
        }
      },
    },
  },
  "components": {
    "schemas": {
      "Pet": {
        "allOf": [
          {
            "$ref": "#/components/schemas/NewPet"
          },
          {
            "type": "object",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "integer",
                "format": "int64"
              }
            }
          }
        ]
      },
      "NewPet": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "tag": {
            "type": "string"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "integer",
            "format": "int32"
          },
          "message": {
            "type": "string"
          }
        }
      }
    }
  }
}

在路径 paths['pets']['get']['responses']['200']['content']['application/json'] 中可以找到我们想要的 Pet 类型,但是,这是json格式,并不是我们想要的ts类型,这里通过一个openapi-typescript 库把json转换为如下的ts类型定义

// type-define.ts

export interface paths {
  "/pets/{id}": {
    /** @description Returns a user based on a single ID, if the user does not have access to the pet */
    get: operations["find pet by id"];
  };
}
​
export type webhooks = Record<string, never>;
​
export interface components {
  schemas: {
    Pet: components["schemas"]["NewPet"] & {
      /** Format: int64 */
      id: number;
    };
    NewPet: {
      name: string;
      tag?: string;
    };
    Error: {
      /** Format: int32 */
      code: number;
      message: string;
    };
  };
  responses: never;
  parameters: never;
  requestBodies: never;
  headers: never;
  pathItems: never;
}
​
export type external = Record<string, never>;
​
export interface operations {
  /** @description Returns a user based on a single ID, if the user does not have access to the pet */
  "find pet by id": {
    parameters: {
      path: {
        /** @description ID of pet to fetch */
        id: number;
      };
    };
    responses: {
      /** @description pet response */
      200: {
        content: {
          "application/json": components["schemas"]["Pet"];
        };
      };
      /** @description unexpected error */
      default: {
        content: {
          "application/json": components["schemas"]["Error"];
        };
      };
    };
  };
}

image-20230425150430587.png

此时,引用上面的ts类型定义就可以开心coding了

import { path } from './type-define'
​
axiosInstance.get<path['/pets/{id}']['get']['responses']['200']['content']['application/json']>('/pets/{id}').then(res => {
  res.data // data type is Pet
})

但是,等等,上面这一长串的路径引用实在是太不美观了吧,虽然可以定义类型别名来缓解,但是还是让人无法接受,我们一开始的目的是自动的类型映射啊。

所以,接下来,在已经有了完善的类型定义的前提下,要怎么把上面这一长串的类型引用省略掉呢?

现在,就轮到强大的 ts 类型体操发挥作用了。在解决一个问题时,通常需要把这个问题简化一下,假如我们需要的Pet类型的并没有隐藏的这么深,只是一层路径,那么我们就可以使用Extract来轻松的提取它

export interface paths {
  "/pet/{id}": {
    get: {
      /** Format: int64 */
      id: number;
      name: string;
      tag?: string;
    };
  };
}
​
type PetsTypes = paths[Extract<keyof paths, "/pet/{id}">]
type PetsGetType = PetsTypes["get"]

image-20230425152511205.png 理论上我们只需要一直Extract,总会得的我们想要的类型,上面我们只是讨论了get 的类型推导,那么post,put 等请求又该怎么处理呢,这里就暂时不再赘述了,

上面的需求现在已经全部封装到 api-typing 库中了,如果感兴趣的话可以去 github 看源码实现,如果对你有所帮助的话,请给个 star 吧。