陪尤雨溪一塊兒,實現 Vuex 無限層級類型推斷。(TS 4.1 新特性)

前言

前幾天,TypeScript 發佈了一項 4.1 版本的新特性,字符串模板類型,尚未了解過的小夥伴能夠先去這篇看一下:TypeScript 4.1 新特性:字符串模板類型,Vuex 終於有救了?前端

本文就利用這個特性,簡單實現下 Vuex 在 modules 嵌套狀況下的 dispatch 字符串類型推斷,先看下效果,咱們有這樣結構的 storegit

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 這個函數,用兩個泛型把 mutationsmodules 經過反向推導給拿到: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>

實現 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 中的全部拼接後的 keypost

推斷 Modules Keys

先提早和大夥同步好,後續泛型裏的: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'

因爲咱們要拿到的是 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 : 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項目實戰」了,很是棒的一本實戰書。

相關文章
相關標籤/搜索