前幾天,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項目實戰」了,很是棒的一本實戰書。