TypeScript基礎看膩了?進階實現智慧類型推導的簡化版Vuex,手把手帶你實現。
- 2020 年 4 月 11 日
- 筆記
之前幾篇講TypeScript的文章中,我帶來了在React中的一些小實踐
React + TypeScript + Hook 帶你手把手打造類型安全的應用。
React Hook + TypeScript 手把手帶你打造use-watch自定義Hook,實現Vue中的watch功能。
這篇文章我決定更進一步,直接用TypeScript實現一個類型安全的簡易版的Vuex。
這篇文章適合誰:
- 已經學習TypeScript基礎,需要一點進階玩法的你。
- 自己喜歡寫一些開源的小工具,需要進階學習TypeScript類型推導。(在項目中一般ts運用的比較淺層,大部分情況在寫表面的interface)。
- 單純的想要進階學習TypeScript。
通過這篇文章,你可以學到以下特性在實戰中是如何使用的:
- ?TypeScript的高級類型(Advanced Type)
- ?TypeScript中利用泛型進行反向類型推導。(Generics)
- ?Mapped types(映射類型)
- ?Distributive Conditional Types(條件類型分配)
- ?TypeScript中Infer的實戰應用(Vue3源碼里infer的一個很重要的使用)
希望通過這篇文章,你可以對TypeScript的高級類型實戰應用得心應手,對於未來想學習Vue3源碼的小夥伴來說,類型推斷和infer
的用法也是必須熟悉的。
寫在前面:
本文實現的Vuex只有很簡單的state
,action
和subscribeAction
功能,因為Vuex當前的組織模式非常不適合類型推導(Vuex官方的type庫目前推斷的也很簡陋),所以本文中會有一些和官方不一致的地方,這些是刻意的為了類型安全而做的,本文的主要目標是學習TypeScript,而不是學習Vuex,所以請小夥伴們不要嫌棄它程式碼啰嗦或者和Vuex不一致。 ?
vuex骨架
首先定義我們Vuex的骨架。
export default class Vuex<S, A> { state: S action: Actions<S, A> constructor({ state, action }: { state: S; action: Actions<S, A> }) { this.state = state; this.action = action; } dispatch(action: any) { } } 複製程式碼
首先這個Vuex構造函數定了兩個泛型S
和A
,這是因為我們需要推出state
和action
的類型,定義action對象的時候需要用到state
的類型,而調用store.dispatch時需要用到action
的key的類型(比如dispatch({type: "ADD"})
中的type需要由對應 actions: { ADD() {} }
)的key值推斷。
然後在構造函數中,把S和state對應,把Actions<S, A>和傳入的action對應。
constructor({ state, action }: { state: S; action: Actions<S, A> }) { this.state = state; this.action = action; } 複製程式碼
Actions這裡用到了映射類型,它等於是遍歷了傳入的A的key值,然後定義每一項實際上的結構,
export type Actions<S, A> = { [K in keyof A]: (state: S, payload: any) => Promise<any>; }; 複製程式碼
看看我們傳入的actions
const store = new Vuex({ state: { count: 0, message: '', }, action: { async ADD(state, payload) { state.count += payload; }, async CHAT(state, message) { state.message = message; }, }, }); 複製程式碼
是不是類型正好對應上了?此時ADD函數的形參里的state就有了類型推斷,它就是我們傳入的state的類型。

這是因為我們給Vuex的構造函數傳入state的時候,S就被反向推導為了state的類型,也就是{count: number, message: string}
,這時S又被傳給了Actions<S, A>
, 自然也可以在action里獲得state的類型了。
現在有個問題,我們現在的寫法里沒有任何地方能體現出payload
的類型,(這也是Vuex設計所帶來的一些缺陷)所以我們也只能寫成any,但是我們本文的目標是類型安全。
dispatch的類型安全
下面先想點辦法實現store.dispatch
的類型安全:
- type需要自動提示。
- type填寫了以後,需要提示對應的payload的type。
所以參考redux
的玩法,我們手動定義一個Action Types的聯合類型。
const ADD = 'ADD'; const CHAT = 'CHAT'; type AddType = typeof ADD; type ChatType = typeof CHAT; type ActionTypes = | { type: AddType; payload: number; } | { type: ChatType; payload: string; }; 複製程式碼
在Vuex
中,我們新增一個輔助Ts推斷的方法,這個方法原封不動的返回dispatch函數,但是用了as
關鍵字改寫它的類型,我們需要把ActionTypes作為泛型傳入:
export default class Vuex<S, A> { ... createDispatch<A>() { return this.dispatch.bind(this) as Dispatch<A>; } } 複製程式碼
Dispatch類型的實現相當簡單,直接把泛型A交給第一個形參action就好了,由於ActionTypes是聯合類型,Ts會嚴格限制我們填寫的action的類型必須是AddType或者ChatType中的一種,並且填寫了AddType後,payload的類型也必須是number了。
export interface Dispatch<A> { (action: A): any; } 複製程式碼
然後使用它構造dispatch
// for TypeScript support const dispatch = store.createDispatch<ActionTypes>(); 複製程式碼
目標達成:


action形參中payload的類型安全
此時雖然store.diaptch完全做到了類型安全,但是在聲明action傳入vuex構造函數的時候,我不想像這樣手動聲明,
const store = new Vuex({ state: { count: 0, message: '', }, action: { async [ADD](state, payload: number) { state.count += payload; }, async [CHAT](state, message: string) { state.message = message; }, }, }); 複製程式碼
因為這個類型在剛剛定義的ActionTypes中已經有了,秉著DRY
的原則,我們繼續折騰吧。
首先現在我們有這些佐料:
const ADD = 'ADD'; const CHAT = 'CHAT'; type AddType = typeof ADD; type ChatType = typeof CHAT; type ActionTypes = | { type: AddType; payload: number; } | { type: ChatType; payload: string; }; 複製程式碼
所以我想通過一個類型工具,能夠傳入AddType
給我返回number
,傳入ChatType
給我返回string
:
它大概是這個樣子的:
type AddPayload = PickPayload<ActionTypes, AddType> // number type ChatPayload = PickPayload<ActionTypes, ChatType> // string 複製程式碼
為了實現它,我們需要用到distributive-conditional-types,不熟悉的同學可以好好看看這篇文章。
簡單的來說,如果我們把一個聯合類型
string | number 複製程式碼
傳遞給一個用了extends關鍵字的類型工具:
type PickString<T> = T extends string ? T: never type T1 = PickString<string | number> // string 複製程式碼
它並不是像我們想像中的直接去用string | number直接匹配是否extends,而是把聯合類型拆分開來,一個個去匹配。
type PickString<T> = | string extends string ? T: never | number extends string ? T: never 複製程式碼
所以返回的類型是string | never
,由由於never在聯合類型中沒什麼意義,所以就被過濾成string
了
藉由這個特性,我們就有思路了,這裡用到了infer
這個關鍵字,Vue3中也有很多推斷是藉助它實現的,它只能用在extends的後面,代表一個還未出現的類型,關於infer的玩法,詳細可以看這篇文章:巧用 TypeScript(五)—- infer
export type PickPayload<Types, Type> = Types extends { type: Type; payload: infer P; } ? P : never; 複製程式碼
我們用Type這個字元串類型,讓ActionTypes中的每一個類型一個個去過濾匹配,比如傳入的是AddType:
PickPayload<ActionTypes, AddType> 複製程式碼
則會被分布成:
type A = | { type: AddType;payload: number;} extends { type: AddType; payload: infer P } ? P : never | { type: ChatType; payload: string } extends { type: AddType; payload: infer P } ? P : never; 複製程式碼
注意infer P的位置,被放在了payload的位置上,所以第一項的type在命中後, P也被自動推斷為了number,而三元運算符的 ? 後,我們正是返回了P,也就推斷出了number這個類型。
這時候就可以完成我們之前的目標了,也就是根據AddType這個類型推斷出payload參數的類型,PickPayload
這個工具類型應該定位成vuex官方倉庫里提供的輔助工具,而在項目中,由於ActionType已經確定,所以我們可以進一步的提前固定參數。(有點類似於函數柯里化)
type PickStorePayload<T> = PickPayload<ActionTypes, T>; 複製程式碼
此時,我們定義一個類型安全的Vuex實例所需要的所有輔助類型都定義完畢:
const ADD = 'ADD'; const CHAT = 'CHAT'; type AddType = typeof ADD; type ChatType = typeof CHAT; type ActionTypes = | { type: AddType; payload: number; } | { type: ChatType; payload: string; }; type PickStorePayload<T> = PickPayload<ActionTypes, T>; 複製程式碼
使用起來就很簡單了:
const store = new Vuex({ state: { count: 0, message: '', }, action: { async [ADD](state, payload: PickStorePayload<AddType>) { state.count += payload; }, async [CHAT](state, message: PickStorePayload<ChatType>) { state.message = message; }, }, }); // for TypeScript support const dispatch = store.createDispatch<ActionTypes>(); dispatch({ type: ADD, payload: 3, }); dispatch({ type: CHAT, payload: 'Hello World', }); 複製程式碼
總結
本文的所有程式碼都在 github.com/sl1673495/t… 倉庫里,裡面還加上了getters的實現和類型推導。
通過本文的學習,相信你會對高級類型的用法有進一步的理解,也會對TypeScript的強大更加嘆服,本文有很多例子都是為了教學而刻意深究,複雜化的,請不要罵我(XD)。
在實際的項目運用中,首先我們應該避免Vuex這種集中化的類型定義,而盡量去擁抱函數(函數對於TypeScript是天然支援),這也是Vue3往函數化api方向走的原因之一。
參考文章
React + Typescript 工程化治理實踐(螞蟻金服的大佬實踐總結總是這麼靠譜) juejin.im/post/5dccc9…
TS 學習總結:編譯選項 && 類型相關技巧 zxc0328.github.io/diary/2019/…
Conditional types in TypeScript(據說比Ts官網講的好) mariusschulz.com/blog/condit…
Conditional Types in TypeScript(文風幽默,程式碼非常硬核) artsy.github.io/blog/2018/1…