📃 前言
在开发自己的开源库(基于ahooks vue的实现)vue-hooks-plus
的 useRequest
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
定义插件,这里我定义了两个插件 pluginA
、pluginB
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
真的让人眼前一亮 😍。
- 另外的一种是一位资深老哥提供的方法,递归类型进行合并,有兴趣的同学研究一下,个人觉得有点饶 😂
🍬 TS 额外零食
// 判断数组长度
T = Plugin[]
T['length'] extends 1 ...条件判断 // 数组长度为1
// 获取数组的最后,或者首个元素的类型
T = Plugin[]
// 首个
T extends [infer First,...RestPlugins extends Plugin[] ] ...条件判断
// 最后一个
T extends [...RestPlugins extends Plugin[],infer Laster] ...条件判断
✨ 结语
今天的分享就到这了,
infer
和extends
我觉得是ts的一个难点,有时候写多了会很绕,也希望看到这篇文章的兄弟姐妹们日后的ts水平突飞猛进!
感兴趣的同学可以去我的开源库vue-hooks-plus
看看这块的实现
📃 文档地址
🌟 Github
大家多多支持 start start 🌟