前几天,TypeScript 发布了一项 4.1 版本的新特性,字符串模板类型,尚未了解过的小伙伴能够先去这篇看一下:TypeScript 4.1 新特性:字符串模板类型,Vuex 终于有救了?。前端
本文就利用这个特性,简单实现下 Vuex 在 modules
嵌套状况下的 dispatch
字符串类型推断,先看下效果,咱们有这样结构的 store
:git
const store = Vuex({ mutations: { root() {}, }, modules: { cart: { mutations: { add() {}, remove() {}, }, }, user: { mutations: { login() {}, }, modules: { admin: { mutations: { login() {}, }, }, }, }, }, })
须要实现这样的效果,在 dispatch
的时候可选的 action
字符串类型要能够被提示出来:github
store.dispatch('root') store.dispatch('cart/add') store.dispatch('user/login') store.dispatch('user/admin/login')
首先先定义好 Vuex 这个函数,用两个泛型把 mutations
和 modules
经过反向推导给拿到:typescript
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>
那么接下来的重点就是实现 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
。post
先提早和大伙同步好,后续泛型里的:ui
Modules
表明 { cart: { modules: {} }, user: { modules: {} }
这种多个 Module
组合的对象结构。Module
表明单个子模块,好比 cart
。利用spa
type Values<Modules> = { [K in keyof Modules]: Modules[K] }[keyof Modules]
这种方式,能够轻松的把对象里的全部值 类型给展开,好比
type Obj = { a: 'foo' b: 'bar' } type T = Values<Obj> // 'foo' | 'bar'
因为咱们要拿到的是 cart
、user
对应的值里提取出来的 key
,
因此利用上面的知识,咱们编写 GetModulesMutationKeys
来获取 Modules
下的全部 key
:
type GetModulesMutationKeys<Modules> = { [K in keyof Modules]: GetModuleMutationKeys<Modules[K], K> }[keyof Modules]
首先利用 K in keyof Modules
来拿到全部的 key,这样咱们就能够拿到 cart
、user
这种单个 Module
,而且传入给 GetModuleMutationKeys
这个类型,K
也要一并传入进去,由于咱们须要利用 cart
、user
这些 key
来拼接在最终获得的类型前面。
接下来实现 GetModuleMutationKeys
,分解一下需求,首先单个 Module
是这样子的:
cart: { mutations: { add() { }, remove() { } } },
那么拿到它的 Mutations
后,咱们只须要去拼接 cart/add
、cart/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 中提到的:
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
拼接出来了。
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")
前往 TypeScript Playground 体验。
这个新特性给 TS 库开发的做者带来了无限可能性,有人用它实现了 URL Parser 和 HTML parser,有人用它实现了 JSON parse
甚至有人用它实现了简单的正则,这个特性让类型体操的爱好者以及框架的库做者能够进一步的大展身手,期待他们写出更增强大的类型库来方便业务开发的童鞋吧~
关注公众号「前端从进阶到入院」,后台回复 TS,送几本「TypeScript项目实战」了,很是棒的一本实战书。