以前几篇讲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…