最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Typescript 泛型包教包会

    正文概述 掘金(FESKY)   2021-04-02   655

    不知道在你的日常工作中,是否出现过这样的场景:明明 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
    }
    
    ...
    

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

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

    代码实现

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

    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 参数类型。而返回结果的类型,三个方法都会用到了。

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

    Typescript 泛型包教包会

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

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

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

    Ending

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


    起源地下载网 » Typescript 泛型包教包会

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元