We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
前几天,TypeScript 发布了一项 4.1 版本的新特性,字符串模板类型,还没有了解过的小伙伴可以先去这篇看一下:TypeScript 4.1 新特性:字符串模板类型,Vuex 终于有救了?。
本文就利用这个特性,简单实现下 Vuex 在 modules 嵌套情况下的 dispatch 字符串类型推断,先看下效果,我们有这样结构的 store:
modules
dispatch
store
const store = Vuex({ mutations: { root() {}, }, modules: { cart: { mutations: { add() {}, remove() {}, }, }, user: { mutations: { login() {}, }, modules: { admin: { mutations: { login() {}, }, }, }, }, }, })
需要实现这样的效果,在 dispatch 的时候可选的 action 字符串类型要可以被提示出来:
action
store.dispatch('root') store.dispatch('cart/add') store.dispatch('user/login') store.dispatch('user/admin/login')
首先先定义好 Vuex 这个函数,用两个泛型把 mutations 和 modules 通过反向推导给拿到:
mutations
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 的时候,就可以智能提示了。
dispatch(action: Action<Mutations, Modules>): void
Action
'root' | 'cart/add' | 'user/login' | 'user/admin/login'
Action 里首先可以简单的先把 keyof Mutations 拿到,因为根 store 下的 mutations 不需要做任何的拼接,
keyof Mutations
重头戏在于,我们需要根据 Modules 这个泛型,也就是对应结构:
Modules
modules: { cart: { mutations: { add() { }, remove() { } } }, user: { mutations: { login() { } }, modules: { admin: { mutations: { login() { } }, } } } }
来拿到 modules 中的所有拼接后的 key。
key
先提前和大伙同步好,后续泛型里的:
{ cart: { modules: {} }, user: { modules: {} }
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,
user
所以利用上面的知识,我们编写 GetModulesMutationKeys 来获取 Modules 下的所有 key:
GetModulesMutationKeys
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 来拼接在最终得到的类型前面。
K in keyof Modules
GetModuleMutationKeys
K
接下来实现 GetModuleMutationKeys,分解一下需求,首先单个 Module 是这样子的:
cart: { mutations: { add() { }, remove() { } } },
那么拿到它的 Mutations 后,我们只需要去拼接 cart/add、cart/remove 即可,那么如何拿到一个对象类型中的 mutations?
Mutations
cart/add
cart/remove
我们用 infer 来取:
infer
type GetMutations<Module> = Module extends { mutations: infer M } ? M : never
然后通过 keyof GetMutations<Module>,即可轻松拿到 'add' | 'remove' 这个类型,我们再实现一个拼接 Key 的类型,注意这里就用到了 TS 4.1 的字符串模板类型了
keyof GetMutations<Module>
'add' | 'remove'
Key
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 中提到的:
${'cart'}/${'add' | 'remove'}
'cart/add' | 'cart/remove'
symbol | number
Keys & string
string
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 拼接出来了。
AddPrefix<Key, keyof GetMutations<Module>>
cart 模块下还可能有别的 Modules,比如这样:
cart: { mutations: { add() { }, remove() { } } modules: { subCart: { mutations: { add() { }, } } } },
其实很简单,我们刚刚已经定义好了从 Modules 中提取 Keys 的工具类型,也就是 GetModulesMutationKeys,只需要递归调用即可,不过这里我们需要做一层预处理,把 modules 不存在的情况给排除掉:
Keys
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 甚至有人用它实现了简单的正则,这个特性让类型体操的爱好者以及框架的库作者可以进一步的大展身手,期待他们写出更加强大的类型库来方便业务开发的童鞋吧~
JSON parse
关注公众号「前端从进阶到入院」,关注第一手干货资讯,还可以获取『高级前端进阶指南』和『前端算法零基础进阶指南』。
The text was updated successfully, but these errors were encountered:
No branches or pull requests
前言
前几天,TypeScript 发布了一项 4.1 版本的新特性,字符串模板类型,还没有了解过的小伙伴可以先去这篇看一下:TypeScript 4.1 新特性:字符串模板类型,Vuex 终于有救了?。
本文就利用这个特性,简单实现下 Vuex 在
modules
嵌套情况下的dispatch
字符串类型推断,先看下效果,我们有这样结构的store
:需要实现这样的效果,在
dispatch
的时候可选的action
字符串类型要可以被提示出来:实现
定义函数骨架
首先先定义好 Vuex 这个函数,用两个泛型把
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
中的所有拼接后的key
。推断 Modules Keys
先提前和大伙同步好,后续泛型里的:
Modules
代表{ cart: { modules: {} }, user: { modules: {} }
这种多个Module
组合的对象结构。Module
代表单个子模块,比如cart
。利用
这种方式,可以轻松的把对象里的所有值 类型给展开,比如
由于我们要拿到的是
cart
、user
对应的值里提取出来的key
,所以利用上面的知识,我们编写
GetModulesMutationKeys
来获取Modules
下的所有key
:首先利用
K in keyof Modules
来拿到所有的 key,这样我们就可以拿到cart
、user
这种单个Module
,并且传入给GetModuleMutationKeys
这个类型,K
也要一并传入进去,因为我们需要利用cart
、user
这些key
来拼接在最终得到的类型前面。推断单个 Module Keys
接下来实现
GetModuleMutationKeys
,分解一下需求,首先单个Module
是这样子的:那么拿到它的
Mutations
后,我们只需要去拼接cart/add
、cart/remove
即可,那么如何拿到一个对象类型中的mutations
?我们用
infer
来取:然后通过
keyof GetMutations<Module>
,即可轻松拿到'add' | 'remove'
这个类型,我们再实现一个拼接Key
的类型,注意这里就用到了 TS 4.1 的字符串模板类型了这里会自动把联合类型展开并分配,
${'cart'}/${'add' | 'remove'}
会被推断成'cart/add' | 'cart/remove'
,不过由于我们传入的是keyof GetMutations<Module>
它还有可能是symbol | number
类型,所以用Keys & string
来取其中的string
类型,这个技巧也是老爷子在 Template string types MR 中提到的:那么,利用
AddPrefix<Key, keyof GetMutations<Module>>
就可以轻松的把cart
模块下的mutations
拼接出来了。推断嵌套 Module Keys
cart
模块下还可能有别的Modules
,比如这样:其实很简单,我们刚刚已经定义好了从
Modules
中提取Keys
的工具类型,也就是GetModulesMutationKeys
,只需要递归调用即可,不过这里我们需要做一层预处理,把modules
不存在的情况给排除掉:利用 extends 去判断类型结构,对不存在
modules
的结构直接返回 never,再用 infer 去提取出 Modules 的结构,并且把前一个模块的key
拼接在刚刚写好的GetModulesMutationKeys
返回的结果之前:以这个
cart
模块为例,分解一下每个工具类型得到的结果:这样,就巧妙的利用递归把无限层级的
modules
拼接实现了。完整代码
前往 TypeScript Playground 体验。
结语
这个新特性给 TS 库开发的作者带来了无限可能性,有人用它实现了 URL Parser 和 HTML parser,有人用它实现了
JSON parse
甚至有人用它实现了简单的正则,这个特性让类型体操的爱好者以及框架的库作者可以进一步的大展身手,期待他们写出更加强大的类型库来方便业务开发的童鞋吧~感谢大家
关注公众号「前端从进阶到入院」,关注第一手干货资讯,还可以获取『高级前端进阶指南』和『前端算法零基础进阶指南』。
The text was updated successfully, but these errors were encountered: