以前幾篇講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>
_actionSubscribers: ActionSubscribers<S, A>
constructor({ state, action }: { state: S; action: Actions<S, A> }) {
this.state = state;
this.action = action;
}
dispatch(action: ActionArguments<A>) {
}
subscribeAction(subscriber: ActionSubscriber<S, A>) {
}
}
複製代碼
首先這個Vuex構造函數定了兩個泛型S
和A
,這是由於咱們須要推出state
和action
的類型,因爲subscribeAction的參數中須要用到state和action的類型,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給我返回message:
它大概是這個樣子的:
type AddPayload = PickPayload<ActionTypes, AddType> // number
type ChatPayload = PickPayload<ActionTypes, ChatType> // string
複製代碼
爲了實現它,咱們須要用到distributive-conditional-types,不熟悉的同窗能夠好好看看這篇文章。
簡單的來講,若是咱們把一個聯合類型
type A = string | number
複製代碼
傳遞給一個用了extends關鍵字的類型工具:
type PickString<T> = T extends string ? T: never
type T1 = PickString<A> // 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…