前言

前几天,TypeScript 公布了一项 4.1 版本的新个性,字符串模板类型,还没有理解过的小伙伴能够先去这篇看一下:TypeScript 4.1 新个性:字符串模板类型,Vuex 终于没救了?。

本文就利用这个个性,简略实现下 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 中提到的:

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 : nevertype AddPrefix<Prefix, Keys> = `${Prefix & string}/${Keys & string}`type GetSubModuleKeys<Module, Key> = Module extends { modules: infer SubModules }   ? AddPrefix<Key, GetModulesMutationKeys<SubModules>>   : nevertype 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我的项目实战」了,十分棒的一本实战书。