Typescript 泛型包教包会

Typescript 泛型包教包会

不知道在你的日常工作中,是否出现过这样的场景:明明 Typescript 官方文档已经看了很多遍,实际写起代码来却各种煎熬,遇到报错,在搜索无果之后,无奈写下 any。🤷‍♀️ (我猜有,不然你也不会点开这篇文章。👻

而阻碍你强类型更近一步的,绝大多数情况下是因为泛型还没完全掌握。这篇文章将从我日常工作中遇到的一个例子入手,一步步介绍哪里需要用到泛型,怎么写~

(如果除了泛型,Typescript 其他知识点也不太熟怎么办 😰 ?可以我之前整理的另一篇比较全面的文章结合实例学习 Typescript

Let's begin。

问题

说,后端提供了多个支持分页查列表数据的接口,这些接口的参数格式、响应结果、分页形式可能都不一样。拿分页形式来说,常见的分页参数类型就有好几种,传页数和每页数量、传偏移值和 limit、使用上一页最后一个 id 来查询等等。

{
  page_size: number,
  page_num: number
}

{
  offset: number,
  limit: number
}

{
  forward: boolean
  last_id: string
  page_size: number
}

...
复制代码

这些接口数据量都在几千条数据左右,考虑数据库的压力,后端同学不建议一次拉几千条数据,需要前端分页去全部拉取。

为了避免分页的逻辑每个接口都写一次,要求实现一个强类型的工具方法,实现自动分页拉取全部数据的功能。

代码实现

这篇文章的重点不在如何实现这样的功能,简单画一下流程图,相信大部分人都能实现。

WX20210401-171127.png

一份可行的代码实现如下:

const unpaginate = (
  api,
  config,
) => {
  const { getParams, hasMore, dataAdaptor } = config

  async function iterator(time, lastRes) {
    // 通过上一次请求结果和第几次请求获取下一次请求的参数
    const params = getParams(lastRes, time)
    const res = await api(params)

    let next = []

    // 如果还有下一页,继续拉取
    if (hasMore(res, params)) {
      next = await iterator(time + 1, res)
    }

    // 拼接结果一起返回
    return dataAdaptor(res).concat(next)
  }

  return iterator()
}
复制代码

代码解读unpaginate 方法第一个参数传入一个返回 Promise 结果的 api 方法;第二个参数支持传入一个可配置对象:

getParams 方法会把上一次请求的结果以及当前是第几次请求回传,方便使用者设置请求参数;
hasMore 方法会回传当前请求的结果和参数,需要使用者告知程序是否已经拉取完毕;
dataAdaptor 方法则把每次请求得到的结果,回传回去允许自定义返回结果的格式(例如把某个字段下划线改成驼峰),并把返回值作为最终结果存下来;

想一想,你在用 Typescript 的时是否也实现过类型的功能,类型安全吗?编码时会有代码提示吗?还是说也是 any 一把梭呢?

接下来,我们将为一步一步为这个方法提供类型支持

Typescritp 泛型加持

首先从参数入手,为 api 和 config 编写最基本的类型声明。

export interface Config {
  hasMore: (res?: any, params?: any) => boolean
  getParams: (res?: any, time?: number) => any
  dataAdaptor: (res: any) => any[]
}

const unpaginate = (
  api: (params: any) => Promise<any[]>,
  config: Config,
): Promise<any[]> => {
  ...
}
复制代码

上面的类型声明能起的作用不大(因为到处是 any),不过也比没有好,至少在给 apiconfig 传不符合类型的参数时会报错。

第一个泛型——参数类型

很容易看到,Config 类型中方法的参数和 api 类型强关联api 的参数的类型决定了 hasMore 方法的 params 参数类型。而返回结果的类型,三个方法都会用到了。

说到方法,在 Typescript 中,可以用 Parameters ReturnType 来从方法的类型上提取参数类型和返回值类型。

type EventListenerParamsType = Parameters<typeof window.addEventListener>;
// [type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | undefined]
	
type A = (a: number) => string
type B = ReturnType<A>
// string
复制代码

而这里 api 不是固定的类型,需要根据动态api 类型上提取类型,泛型登场。

const unpaginate = <T extends (params: any) => Promise<any>>(
  api: T,
  config: Config,
): Promise<any[]> => {
  ...
}
复制代码

我们在方法前加上了 <T extends (params: any) => Promise<any>> 这段代码,表示声明了一个泛型,extends 限制了这个泛型的下限:必须是一个方法,并且返回一个 Promise 结果。

然后又将 T 类型赋予 api,这样写完后面再使用类型 T,Typescript 就动态地根据实际调用的 api 方法类型自动推导了。

api 是泛型,Config 当然也需要是泛型,泛型是当做参数可以传递的

export interface Config<P> {
  hasMore: (res?: R, params?: P) => boolean
  //  ...
}
复制代码

interface Config<P> 这里我们让 Config 也支持了泛型参数,将其传给了 parmas 参数。可以认为这里的 P 只是随意起的变量名,换成 T 也是可以的。

结合 Parameters 泛型工具方法,取 T 的第一个参数类型传给 Config,这样它们的类型就关联起来了。


const unpaginate = <T extends (params: any) => Promise<any>>(
  api: T,
  config: Config<Parameters<T>[0]>,
): Promise<any[]> => {
  ...
}
复制代码

Parameters<T>[0] 的意思是,取 T 类型的参数(是一个数组类型)的第一个参数类型。

第二个泛型——返回值的类型

参数类型能动态推导出来,按道理 api 的返回结果也可以使用同样的操作实现。

不过这里会遇到一个棘手的问题,api 返回结果的类型是 Promsie<R>,而 config 回传回去的结果应该去 Promise 化的 R 类型。

从泛型中提取类型,我们会用到 infer,直接看代码吧:

type UnPromise<T> = T extends Promise<infer U> ? U : undefined

type A = Promise<number>
type B = UnPromise<A>
// number
复制代码

如果说泛型是动态类型,infer 就是动态的动态类型。上面的例子中,我们在 extends 子句中使用,告诉 Typescript 这里的类型需要动态推导一下。

提取出了返回值的实体类型,继续完善类型定义:

export interface Config<P, R> {
  hasMore: (res?: R, params?: P) => boolean

  getParams: (res?: R, time?: number) => Partial<P>

  dataAdaptor: (res: R) => any[]
}

type UnPromise<T> = T extends Promise<infer U> ? U : undefined

const unpaginate = <
  T extends (params: any) => Promise<any>,
  U extends UnPromise<ReturnType<T>>
>(
  api: T,
  config: Config<Parameters<T>[0], U>,
): Promise<any[]> => {
  ...
}
复制代码

第二个泛型 U 是动态从 UnPromise<ReturnType<T>> 推导出来的,然后再将其传递给 Config 就完成了返回结果的类型传导。

第三个泛型——格式化后的结果类型

剩下最后一个要处理的问题,是 dataAdaptor 的返回值结果类型。我们对其返回结果没有任何限制,需要做的也是让 Typescirpt 自行推导和传递。 并做为 unpaginate 方法的返回结果类型。

这里需要再定义一个泛型:

export interface Config<P, R, V> {
  //  ...
  dataAdaptor: (res: R) => V[]
}

const unpaginate = <
  T extends (params: any) => Promise<any>,
  U extends UnPromise<ReturnType<T>>,
  V extends any
>(
  api: T,
  config: Config<Parameters<T>[0], U, V>,
): Promise<V[]>
复制代码

我们使用 V extends any 定义了新的泛型类型,将其传递给 Config.dataAdaptor 的返回结果,dataAdaptor: (res: R) => V[] 这样 Typescript 在具体的场景下就可以根据 dataAdaptor 返回的数组类型 => 推导出 V 的类型了。

再将 V[] 作为 unpaginate 的返回值类型,这样就可以全串起来了。

最终效果

API 方法参数推导:

WX20210401-183613.png

API 方法返回结果推导: WX20210401-183554.png

格式化后返回结果推导: WX20210401-183347.png

可以在Typescript playground 上体验,代码也可以在我的 github 上找到。

Ending

这篇文章通过一步步介绍如何使用泛型为一个通用方法实现类型声明,希望看完之后对你有所帮助。对 Typescript 还不太熟悉的同学可以看我之前写的另一篇文章《结合实例学习 Typescript》

分类:
前端