陪尤雨溪一起,实现 Vuex 无限层级类型推断。(TS 4.1 新特性)

简介: 前几天,TypeScript 发布了一项 4.1 版本的新特性,字符串模板类型,还没有了解过的小伙伴可以先去这篇看一下:TypeScript 4.1 新特性:字符串模板类型,Vuex 终于有救了?[1]。本文就利用这个特性,简单实现下 Vuex 在 modules 嵌套情况下的 dispatch 字符串类型推断,先看下效果,我们有这样结构的 store


前言


前几天,TypeScript 发布了一项 4.1 版本的新特性,字符串模板类型,还没有了解过的小伙伴可以先去这篇看一下:TypeScript 4.1 新特性:字符串模板类型,Vuex 终于有救了?[1]

本文就利用这个特性,简单实现下 Vuex 在 modules 嵌套情况下的 dispatch 字符串类型推断,先看下效果,我们有这样结构的 store

const store = Vuex({
  mutations: {
    root() {},
  },
  modules: {
    cart: {
      mutations: {
        add() {},
        remove() {},
      },
    },
    user: {
      mutations: {
        login() {},
      },
      modules: {
        admin: {
          mutations: {
            login() {},
          },
        },
      },
    },
  },
})

需要实现这样的效果,在 dispatch 的时候可选的 action 字符串类型要可以被提示出来:

store.dispatch('root')
store.dispatch('cart/add')
store.dispatch('user/login')
store.dispatch('user/admin/login')


实现


定义函数骨架


首先先定义好 Vuex 这个函数,用两个泛型把 mutationsmodules 通过反向推导给拿到:

type Store<Mutations, Modules> = {
  // 下文会实现这个 Action 类型
  dispatch(action: Action<Mutations, Modules>): void
}
type VuexOptions<Mutations, Modules> = {
  mutations: Mutations
  modules: Modules
}
declare function Vuex<Mutations, Modules>(
  options: VuexOptions<Mutations, Modules>
): Store<Mutations, Modules>


实现 Action


那么接下来的重点就是实现 dispatch(action: Action<Mutations, Modules>): void 中的 Action 了,我们的目标是把他推断成一个 'root' | 'cart/add' | 'user/login' | 'user/admin/login' 这样的联合类型,这样用户在调用 dispatch 的时候,就可以智能提示了。

Action 里首先可以简单的先把 keyof Mutations 拿到,因为根 store 下的 mutations 不需要做任何的拼接,

重头戏在于,我们需要根据 Modules 这个泛型,也就是对应结构:

modules: {
   cart: {
      mutations: {
         add() { },
         remove() { }
      }
   },
   user: {
      mutations: {
         login() { }
      },
      modules: {
         admin: {
            mutations: {
               login() { }
            },
         }
      }
   }
}

来拿到 modules 中的所有拼接后的 key


推断 Modules Keys


先提前和大伙同步好,后续泛型里的:

  • Modules 代表 { cart: { modules: {} }, user: { modules: {} } 这种多个 Module 组合的对象结构。
  • Module 代表单个子模块,比如 cart

利用

type Values<Modules> = {
  [K in keyof Modules]: Modules[K]
}[keyof Modules]

这种方式,可以轻松的把对象里的所有 类型给展开,比如

type Obj = {
  a: 'foo'
  b: 'bar'
}
type T = Values<Obj> // 'foo' | 'bar'

由于我们要拿到的是 cartuser 对应的值里提取出来的 key

所以利用上面的知识,我们编写 GetModulesMutationKeys 来获取 Modules 下的所有 key

type GetModulesMutationKeys<Modules> = {
  [K in keyof Modules]: GetModuleMutationKeys<Modules[K], K>
}[keyof Modules]

首先利用 K in keyof Modules 来拿到所有的 key,这样我们就可以拿到 cartuser 这种单个 Module,并且传入给 GetModuleMutationKeys 这个类型,K 也要一并传入进去,因为我们需要利用 cartuser 这些 key 来拼接在最终得到的类型前面。


推断单个 Module Keys


接下来实现 GetModuleMutationKeys,分解一下需求,首先单个 Module 是这样子的:

cart: {
   mutations: {
      add() { },
      remove() { }
   }
},

那么拿到它的 Mutations 后,我们只需要去拼接 cart/addcart/remove 即可,那么如何拿到一个对象类型中的 mutations

我们用 infer 来取:

type GetMutations<Module> = Module extends { mutations: infer M } ? M : never

然后通过 keyof GetMutations<Module>,即可轻松拿到 'add' | 'remove' 这个类型,我们再实现一个拼接 Key 的类型,注意这里就用到了 TS 4.1 的字符串模板类型了

type AddPrefix<Prefix, Keys> = `${Prefix}/${Keys}`

这里会自动把联合类型展开并分配,${'cart'}/${'add' | 'remove'} 会被推断成 'cart/add' | 'cart/remove',不过由于我们传入的是 keyof GetMutations<Module> 它还有可能是 symbol | number 类型,所以用 Keys & string 来取其中的 string 类型,这个技巧也是老爷子在 Template string types MR[2] 中提到的:

Above, a keyof T & string intersection is required because keyof T could contain symbol types that cannot be transformed using template string types.

type AddPrefix<Prefix, Keys> = `${Prefix & string}/${Keys & string}`

那么,利用 AddPrefix<Key, keyof GetMutations<Module>> 就可以轻松的把 cart 模块下的 mutations 拼接出来了。


推断嵌套 Module Keys


cart 模块下还可能有别的 Modules,比如这样:

cart: {
   mutations: {
      add() { },
      remove() { }
   }
   modules: {
      subCart: {
       mutations: {
          add() { },
        }
      }
   }
},

其实很简单,我们刚刚已经定义好了从 Modules 中提取 Keys 的工具类型,也就是 GetModulesMutationKeys,只需要递归调用即可,不过这里我们需要做一层预处理,把 modules 不存在的情况给排除掉:

type GetModuleMutationKeys<Module, Key> =
  // 这里直接拼接 key/mutation
  | AddPrefix<Key, keyof GetMutations<Module>>
  // 这里对子 modules 做 keys 的提取
  | GetSubModuleKeys<Module, Key>

利用 extends 去判断类型结构,对不存在 modules 的结构直接返回 never,再用 infer 去提取出 Modules 的结构,并且把前一个模块的 key 拼接在刚刚写好的 GetModulesMutationKeys 返回的结果之前:

type GetSubModuleKeys<Module, Key> = Module extends { modules: infer SubModules }
  ? AddPrefix<Key, GetModulesMutationKeys<SubModules>>
  : never

以这个 cart 模块为例,分解一下每个工具类型得到的结果:

cart: {
   mutations: {
      add() { },
      remove() { }
   }
   modules: {
      subCart: {
       mutations: {
          add() { },
        }
      }
   }
},
type GetModuleMutationKeys<Module, Key> =
    // 'cart/add' | 'cart | remove'
    AddPrefix<Key, keyof GetMutations<Module>> |
    // 'cart/subCart/add'
    GetSubModuleKeys<Module, Key>
type GetSubModuleKeys<Module, Key> = Module extends { modules: infer SubModules }
   ? AddPrefix<
       // 'cart'
       Key,
       // 'subCart/add'
       GetModulesMutationKeys<SubModules>
   >
   : never

这样,就巧妙的利用递归把无限层级的 modules 拼接实现了。


完整代码


type GetMutations<Module> = Module extends { mutations: infer M } ? M : never
type AddPrefix<Prefix, Keys> = `${Prefix & string}/${Keys & string}`
type GetSubModuleKeys<Module, Key> = Module extends { modules: infer SubModules }
   ? AddPrefix<Key, GetModulesMutationKeys<SubModules>>
   : never
type GetModuleMutationKeys<Module, Key> = AddPrefix<Key, keyof GetMutations<Module>> | GetSubModuleKeys<Module, Key>
type GetModulesMutationKeys<Modules> = {
   [K in keyof Modules]: GetModuleMutationKeys<Modules[K], K>
}[keyof Modules]
type Action<Mutations, Modules> = keyof Mutations | GetModulesMutationKeys<Modules>
type Store<Mutations, Modules> = {
   dispatch(action: Action<Mutations, Modules>): void
}
type VuexOptions<Mutations, Modules> = {
   mutations: Mutations,
   modules: Modules
}
declare function Vuex<Mutations, Modules>(options: VuexOptions<Mutations, Modules>): Store<Mutations, Modules>
const store = Vuex({
   mutations: {
      root() { },
   },
   modules: {
      cart: {
         mutations: {
            add() { },
            remove() { }
         }
      },
      user: {
         mutations: {
            login() { }
         },
         modules: {
            admin: {
               mutations: {
                  login() { }
               },
            }
         }
      }
   }
})
store.dispatch("root")
store.dispatch("cart/add")
store.dispatch("user/login")
store.dispatch("user/admin/login")


结语


这个新特性给 TS 库开发的作者带来了无限可能性,有人用它实现了 URL Parser 和 HTML parser[4],有人用它实现了 JSON parse 甚至有人用它实现了简单的正则[5],这个特性让类型体操的爱好者以及框架的库作者可以进一步的大展身手,期待他们写出更加强大的类型库来方便业务开发的童鞋吧~

相关文章
|
20天前
|
前端开发 JavaScript 测试技术
"React新手入门的神奇之处:如何用React构建第一个应用,让你的项目一鸣惊人?"
【8月更文挑战第31天】本文详细介绍了如何从头开始构建首个React应用。React作为当今Web开发中备受欢迎的前端框架,采用组件化设计实现界面构建,便于维护与扩展。文章首先解释了React的基础概念,接着演示了使用Create React App搭建项目的步骤,展示了基础组件编写方法及React Router的简单运用,并分享了一些实用的最佳实践建议,帮助读者快速上手React开发。
26 0
|
21天前
|
设计模式 JavaScript 前端开发
Vue.js 与第三方库的奇妙融合,Bootstrap 和 Vuex 究竟能带来何种意想不到的变革?
【8月更文挑战第30天】在现代Web开发中,结合使用JavaScript设计模式与框架如Vue.js能显著提升代码质量和项目的可维护性。本文探讨了常见JavaScript设计模式及其在Vue.js中的应用。通过具体示例介绍了工厂模式、单例模式和策略模式的应用场景及其实现方法。例如,工厂模式通过`NavFactory`根据用户角色动态创建不同的导航栏组件;单例模式则通过全局事件总线`eventBus`实现跨组件通信;策略模式用于处理不同的表单验证规则。这些设计模式的应用不仅提高了代码的复用性和可维护性,还使得Vue应用更加模块化和灵活。
12 0
|
3月前
|
JavaScript 前端开发 编译器
跨越时代的框架对决:深度剖析Vue 2与Vue 3核心差异
跨越时代的框架对决:深度剖析Vue 2与Vue 3核心差异
58 0
|
4月前
|
前端开发 JavaScript 安全
【亮剑】探讨了在React TypeScript应用中如何通过道具(props)传递CSS样式,以实现模块化、主题化和动态样式
【4月更文挑战第30天】本文探讨了在React TypeScript应用中如何通过道具(props)传递CSS样式,以实现模块化、主题化和动态样式。文章分为三部分:首先解释了样式传递的必要性,包括模块化、主题化和动态样式以及TypeScript集成。接着介绍了内联样式的基本用法和最佳实践,展示了一个使用内联样式自定义按钮颜色的例子。最后,讨论了使用CSS模块和TypeScript接口处理复杂样式的方案,强调了它们在组织和重用样式方面的优势。结合TypeScript,确保了样式的正确性和可维护性,为开发者提供了灵活的样式管理策略。
59 0
|
前端开发
前端学习笔记202306学习笔记第四十二天-ES10增加的新特性1
前端学习笔记202306学习笔记第四十二天-ES10增加的新特性1
43 0
|
前端开发
前端学习笔记202306学习笔记第四十二天-ES10增加的新特性2
前端学习笔记202306学习笔记第四十二天-ES10增加的新特性2
43 1
|
前端开发
前端学习笔记202306学习笔记第四十二天-ES10增加的新特性3
前端学习笔记202306学习笔记第四十二天-ES10增加的新特性3
28 0
|
前端开发 JavaScript 小程序
每天3分钟,重学ES6-ES12(十七)模块化历史
每天3分钟,重学ES6-ES12(十七)模块化历史
75 0
|
存储 JavaScript 编译器
Vue项目实战——实现一个任务清单(学以致用,两小时带你巩固和强化Vue知识点)
Vue项目实战——实现一个任务清单(学以致用,两小时带你巩固和强化Vue知识点)
278 0
|
JavaScript API 容器
一道价值25k的腾讯递归组件面试题(Vue3 + TS 实现)
小伙伴们好久不见,最近刚入职新公司,需求排的很满,平常是实在没时间写文章了,更新频率会变得比较慢。 周末在家闲着无聊,突然小弟过来紧急求助,说是面试腾讯的时候,对方给了个 Vue 的递归菜单要求实现,回来找我复盘。 正好这周是小周,没想着出去玩,就在家写写代码吧,我看了一下需求,确实是比较复杂,需要利用好递归组件,正好趁着这个机会总结一篇 Vue3 + TS 实现递归组件的文章。