以前幾篇講TypeScript的文章中,我帶來了在React中的一些小實踐html
React + TypeScript + Hook 帶你手把手打造類型安全的應用。vue
React Hook + TypeScript 手把手帶你打造use-watch自定義Hook,實現Vue中的watch功能。react
這篇文章我決定更進一步,直接用TypeScript實現一個類型安全的簡易版的Vuex。git
但願經過這篇文章,你能夠對TypeScript的高級類型實戰應用駕輕就熟,對於將來想學習Vue3源碼的小夥伴來講,類型推斷和infer
的用法也是必須熟悉的。github
本文實現的Vuex只有很簡單的state
,action
和subscribeAction
功能,由於Vuex當前的組織模式很是不適合類型推導(Vuex官方的type庫目前推斷的也很簡陋),因此本文中會有一些和官方不一致的地方,這些是刻意的爲了類型安全而作的,本文的主要目標是學習TypeScript,而不是學習Vuex,因此請小夥伴們不要嫌棄它代碼囉嗦或者和Vuex不一致。 🚀vuex
首先定義咱們Vuex的骨架。typescript
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值推斷。redux
而後在構造函數中,把S和state對應,把Actions<S, A>和傳入的action對應。segmentfault
constructor({ state, action }: { state: S; action: Actions<S, A> }) { this.state = state; this.action = action; } 複製代碼
Actions這裏用到了映射類型,它等因而遍歷了傳入的A的key值,而後定義每一項實際上的結構,api
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,可是咱們本文的目標是類型安全。
下面先想點辦法實現store.dispatch
的類型安全:
因此參考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>(); 複製代碼
目標達成:
此時雖然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.cn/post/684490…
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…