likes
comments
collection
share

Typescript 泛型包教包会

作者站长头像
站长
· 阅读数 62

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

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

Let's begin。

问题

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

{
  page_size: number,
  page_num: number
}

{
  offset: number,
  limit: number
}

{
  forward: boolean
  last_id: string
  page_size: number
}

...

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

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

代码实现

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

Typescript 泛型包教包会

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

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 类型。

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 方法参数推导:

Typescript 泛型包教包会

API 方法返回结果推导: Typescript 泛型包教包会

格式化后返回结果推导: Typescript 泛型包教包会

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

Ending

转载自:https://juejin.cn/post/6946144546518466573
评论
请登录