陪尤雨溪一起,实现 Vuex 无限层级类型推断。(TS 4.1 新特性)
前言
前几天,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 这个函数,用两个泛型把 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[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],这个特性让类型体操的爱好者以及框架的库作者可以进一步的大展身手,期待他们写出更加强大的类型库来方便业务开发的童鞋吧~
抽奖啦~
《TypeScript项目开发实战》是一本TypeScript进阶实践指南,通过9个实用项目,详细讲解如何使用TypeScript和不同的JavaScript框架开发高质量的应用程序。书中不仅介绍TypeScript的核心概念与技术,还涵盖Angular和React的一些新功能,以及GraphQL、微服务和机器学习等相关的新技术。
【全书共10章】:第1章 介绍你之前可能没有接触过的TypeScript功能
第2章 将编写第一个实用的项目——一个简单的markdown编辑器
第3章 将使用流行的React库构建一个联系人管理器
第4章 介绍MEAN栈
第5章 介绍如何使用GraphQL和Apollo创建Angular待办事项应用程序
第6章 介绍如何使用Socket.IO构建一个聊天室应用程序
第7章 介绍如何使用必应地图和Firebase创建基于云的Angular地图应用程序
第8章 介绍如何使用一个等效的基于React的栈
第9章 介绍如何使用TensorFlow.js在Web浏览器中托管机器学习
第10章 介绍如何使用ASP.NET Core和免费的Discogs音乐API来编写一个音乐库应用程序。
【通过阅读本书,你将学到】:
-
使用TypeScript和常用模式编写代码。 -
在TypeScript中使用流行的框架和库。 -
使用TypeScript来利用服务器和客户端的功能。 -
应用令人兴奋的新范式,如GraphQL和TensorFlow。 -
使用流行的、基于云的身份验证服务。 -
结合TypeScript和C#来创建ASP.NET Core应用程序。
【本书面向的读者】
本书的读者应该至少已经熟悉TypeScript的基础知识。如果你知道如何使用TypeScript编译器tsc来构建配置文件和编译代码,也知道TypeScript中的类型安全、函数和类等基础知识,那将大有裨益。即使你对TypeScript有比较深入的了解,本书中也会介绍一些你以前可能没有使用过的技术,你应该会对这些资料感兴趣。
奖项设置:
一等奖:「TypeScript项目开发实战」共 3 位。
参与奖:5 元现金红包共 5 位。
一等奖:「TypeScript项目开发实战」共 3 位。
参与奖:5 元现金红包共 5 位。
参与方式:
在公众号后台回复“TS”,参与抽奖,抽取一等奖。
点亮本文在看,随机抽取一等奖。(1、2两步都做,概率更高哦)
参与奖:点亮本文在看,随机抽取 5 位在看的同学送出 10 元现金红包。
温馨提示:
请在开奖前添加我的微信,否则无法领取奖品~
TypeScript 4.1 新特性:字符串模板类型,Vuex 终于有救了?: https://juejin.im/post/6867785919693832200
[2]Template string types MR: https://github.com/microsoft/TypeScript/pull/40336?from=groupmessage
本文分享自微信公众号 - 前端从进阶到入院(code_with_love)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

