前言
前几天,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 这个函数,用两个泛型把 mutations
和 modules
通过反向推导给拿到:
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'
因为咱们要拿到的是 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
来拼接在最终失去的类型后面。
推断单个 Module Keys
接下来实现 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
拼接进去了。
推断嵌套 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")
返回 TypeScript Playground 体验。
结语
这个新个性给 TS 库开发的作者带来了有限可能性,有人用它实现了 URL Parser 和 HTML parser,有人用它实现了 JSON parse
甚至有人用它实现了简略的正则,这个个性让类型体操的爱好者以及框架的库作者能够进一步的大展身手,期待他们写出更加弱小的类型库来不便业务开发的童鞋吧~
谢谢大家
关注公众号「前端从进阶到入院」,后盾回复 TS,送几本「TypeScript 我的项目实战」了,十分棒的一本实战书。