TS infer YYDS !

449 阅读4分钟

📃 前言

在开发自己的开源库(基于ahooks vue的实现)vue-hooks-plususeRequest hook的时候,设计了插件功能,插件是按规则结构的函数,可以运行在 useRequest 中的各个生命周期,如 onBefore 请求前的时候,onSuccess 请求成功的时候进行触发,其他周期详情请见最下方文档 👇

📌 例子展示

<script lang="ts" setup>
  import { useRequest } from 'vue-hooks-plus'
  import { Plugin } from '../../../types'
​
  const useFormatter: Plugin<
    {
      name: string
      age: number
    },
    [],
    {
      // 插件配置的类型
      formatter?: ({ name, age }?: { name: string; age: number }) => any
    }
  > = (fetchInstance, { formatter }) => {
    return {
      onSuccess: () => {
        fetchInstance.setData(formatter?.(fetchInstance.state.data), 'data')
      },
    }
  }
  
  function getUsername(): Promise<{ name: string; age: number }> {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve({
          name: 'yong_git',
          age: 18,
        })
      }, 1000)
    })
  }
​
  const { data } = useRequest(
    () => getUsername(),
    {
     // 这个是 useRequest 默认的配置
     manual:false,
      // 插件配置的类型反馈到这里
      formatter: () => {
        return {
          name: 'plugins update',
          age: 20,
        }
      },
    },
    [useFormatter],
  )

🙋 问题

如上图例子,useFormatter 传入了 useRequest 的第三个参数, useFormatter 的 第三个泛型定义的 formatter 的类型是作用于 useFormatter 的 第二个参数里面的 formatter ,那么在 useFormatter 就可以正常使用这个已经具备类型的 formatter 了,这是泛型的常见用法。

那么我们再接上 useRequest ,上图可以看到,formatter 作为第二个参数里面的一个对象,实际上是定义了一个函数,在运行插件的时候将这个函数注入到插件中,插件再进行调用,如上图是在onSuccess 里面调用 formatter函数,这个函数是由 useRequest 传入,那么问题来了,我如何在传入插件的时候可以读取出它的配置,也就是上图的 formatter 类型,将它给到 useRequest 的第二个参数中,通俗点说就是 插件定义的配置类型合并到 useRequest的配置中。useRequest本身含有默认的配置,详情看最下方文档 👇

const { data } = useRequest(
  () => getUsername(),
  {
    // 这个是 useRequest 默认的配置
     manual:false,
    // 这里需要有和插件一致的类型提示
    formatter: () => {
      return {
        name: 'plugins update',
        age: 20,
      }
    },
  },
  [useFormatter]
)
​

这个问题很有意思,你需要拿到插件数组 [useFormatter] 里面的每个插件,并且取出它们的第二个参数,也就是它们的配置项,然后和 useRequest 的默认配置进行合并,见上图 manual 为内置的类型,formatter 是插件类型,并且支持多插件,我写了一个小例子着重给大家分享👇。

📹 问题重现

定义插件的类型 Plugin

type Plugin<P extends Record<string, any> = any> = (instance: any, options: P) => any

定义插件,这里我定义了两个插件 pluginApluginB

const pluginA: Plugin<{
  formatterString?: ({ name }: { name: string }) => string
}> = (instance: any, { formatterString }) => {
  return {
    onBefore() {
      instance.setData(formatterString?.(instance.data))
    },
  }
}
​
const pluginB: Plugin<{
  formatterNum?: ({ age }: { age: number }) => number
}> = (instance: any, { formatterNum }) => {
  return {
    onBefore() {
      instance.setData(formatterNum?.(instance.data))
    },
  }
}

定义一个类似 useRequest的函数和默认配置的类型 Options,无功能,仅作ts 例子

​
type Options = {
  manual?: boolean
}
export const useRequest = <P>(
  url: string,
  options: Options,
  plugins: P,
) => {
  return {}
}

使用

// 使用
useRequest('url', {}, [pluginA, pluginB])

至此我们的例子就搭建完成 ✅ , 但我们在第二个参数中是没有任何插件的类型提示的,只有内置的类型提示。

👆案例在线地址

👨‍🏫 解决

type MergeOptions<T, K extends Record<string, any>> = T &
  {
    [X in keyof K]: K[X]
  }
​
export const useRequest = <P extends Plugin[]>(
  url: string,
  options: MergeOptions<
    Options,
    P extends (infer H)[] ? (H extends Plugin<infer R> ? R : never) : never
  >,
  plugins: P,
) => {
  return {}
}

上图给useRequest定义了一个插件泛型 P, 继承 Plugin[ ], P extends (infer H)[] 的意思是自动推断出 P 继承的类型 (infer H)[] 就相当于 Plugin[] ,H 就为 Plugin 类型,然后我们继续推断一层 H extends Plugin<infer R> ? R : never出 R,再回见上方的 Plugin 类型,它的泛型就是配置的类型,所以推断出来的 R 就是这个插件配置的类型,然后返回这个R类型。

MergeOptions 进行合并,因为R是一个对象或者是,所以我们需要遍历出 R进行合并,也可以直接 T & K,这里只需要一个联合类型就好了,到这里我们就完成了合并的操作,但实际上这种方法是一个或的操作,目前我实现的比较简洁的一种方式。infer真的让人眼前一亮 😍。

  • 另外的一种是一位资深老哥提供的方法,递归类型进行合并,有兴趣的同学研究一下,个人觉得有点饶 😂

WechatIMG125049.jpeg

🍬 TS 额外零食

// 判断数组长度
T = Plugin[]
T['length'] extends 1 ...条件判断 // 数组长度为1// 获取数组的最后,或者首个元素的类型
T = Plugin[]
// 首个
T extends [infer First,...RestPlugins extends Plugin[] ] ...条件判断
// 最后一个
T extends [...RestPlugins extends Plugin[],infer Laster] ...条件判断

✨ 结语

今天的分享就到这了,inferextends 我觉得是ts的一个难点,有时候写多了会很绕,也希望看到这篇文章的兄弟姐妹们日后的ts水平突飞猛进!
感兴趣的同学可以去我的开源库 vue-hooks-plus 看看这块的实现

📃 文档地址

🌟 Github

大家多多支持 start start 🌟

iShot2022-09-08 13.48.39.png