前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >TypeScript基础看腻了?进阶实现智能类型推导的简化版Vuex,手把手带你实现。

TypeScript基础看腻了?进阶实现智能类型推导的简化版Vuex,手把手带你实现。

作者头像
ssh_晨曦时梦见兮
发布2020-04-11 20:59:16
8280
发布2020-04-11 20:59:16
举报
文章被收录于专栏:前端从进阶到入院

之前几篇讲TypeScript的文章中,我带来了在React中的一些小实践

React + TypeScript + Hook 带你手把手打造类型安全的应用。

React Hook + TypeScript 手把手带你打造use-watch自定义Hook,实现Vue中的watch功能。

这篇文章我决定更进一步,直接用TypeScript实现一个类型安全的简易版的Vuex。

这篇文章适合谁:

  1. 已经学习TypeScript基础,需要一点进阶玩法的你。
  2. 自己喜欢写一些开源的小工具,需要进阶学习TypeScript类型推导。(在项目中一般ts运用的比较浅层,大部分情况在写表面的interface)。
  3. 单纯的想要进阶学习TypeScript。

通过这篇文章,你可以学到以下特性在实战中是如何使用的:

  1. ?TypeScript的高级类型(Advanced Type
  2. ?TypeScript中利用泛型进行反向类型推导。(Generics)
  3. ?Mapped types(映射类型)
  4. ?Distributive Conditional Types(条件类型分配)
  5. ?TypeScript中Infer的实战应用(Vue3源码里infer的一个很重要的使用

希望通过这篇文章,你可以对TypeScript的高级类型实战应用得心应手,对于未来想学习Vue3源码的小伙伴来说,类型推断和infer的用法也是必须熟悉的。

写在前面:

本文实现的Vuex只有很简单的stateactionsubscribeAction功能,因为Vuex当前的组织模式非常不适合类型推导(Vuex官方的type库目前推断的也很简陋),所以本文中会有一些和官方不一致的地方,这些是刻意的为了类型安全而做的,本文的主要目标是学习TypeScript,而不是学习Vuex,所以请小伙伴们不要嫌弃它代码啰嗦或者和Vuex不一致。 ?

vuex骨架

首先定义我们Vuex的骨架。

代码语言:javascript
复制
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构造函数定了两个泛型SA,这是因为我们需要推出stateaction的类型,定义action对象的时候需要用到state的类型,而调用store.dispatch时需要用到action的key的类型(比如dispatch({type: "ADD"})中的type需要由对应 actions: { ADD() {} })的key值推断。

然后在构造函数中,把S和state对应,把Actions<S, A>和传入的action对应。

代码语言:javascript
复制
constructor({ state, action }: { state: S; action: Actions<S, A> }) {
  this.state = state;
  this.action = action;
}
复制代码

Actions这里用到了映射类型,它等于是遍历了传入的A的key值,然后定义每一项实际上的结构,

代码语言:javascript
复制
export type Actions<S, A> = {
  [K in keyof A]: (state: S, payload: any) => Promise<any>;
};
复制代码

看看我们传入的actions

代码语言:javascript
复制
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的类型安全:

  1. type需要自动提示。
  2. type填写了以后,需要提示对应的payload的type。

所以参考redux的玩法,我们手动定义一个Action Types的联合类型。

代码语言:javascript
复制
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作为泛型传入:

代码语言:javascript
复制
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了。

代码语言:javascript
复制
export interface Dispatch<A> {
  (action: A): any;
}
复制代码

然后使用它构造dispatch

代码语言:javascript
复制
// for TypeScript support
const dispatch = store.createDispatch<ActionTypes>();
复制代码

目标达成:

action形参中payload的类型安全

此时虽然store.diaptch完全做到了类型安全,但是在声明action传入vuex构造函数的时候,我不想像这样手动声明,

代码语言:javascript
复制
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的原则,我们继续折腾吧。

首先现在我们有这些佐料:

代码语言:javascript
复制
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

它大概是这个样子的:

代码语言:javascript
复制
type AddPayload = PickPayload<ActionTypes, AddType> // number
type ChatPayload = PickPayload<ActionTypes, ChatType> // string
复制代码

为了实现它,我们需要用到distributive-conditional-types,不熟悉的同学可以好好看看这篇文章。

简单的来说,如果我们把一个联合类型

代码语言:javascript
复制
string | number
复制代码

传递给一个用了extends关键字的类型工具:

代码语言:javascript
复制
type PickString<T> = T extends string ? T: never

type T1 = PickString<string | number> // string
复制代码

它并不是像我们想象中的直接去用string | number直接匹配是否extends,而是把联合类型拆分开来,一个个去匹配。

代码语言:javascript
复制
type PickString<T> = 
| string extends string ? T: never 
| number extends string ? T: never
复制代码

所以返回的类型是string | never,由由于never在联合类型中没什么意义,所以就被过滤成string

借由这个特性,我们就有思路了,这里用到了infer这个关键字,Vue3中也有很多推断是借助它实现的,它只能用在extends的后面,代表一个还未出现的类型,关于infer的玩法,详细可以看这篇文章:巧用 TypeScript(五)---- infer

代码语言:javascript
复制
export type PickPayload<Types, Type> = Types extends {
  type: Type;
  payload: infer P;
}
  ? P
  : never;
复制代码

我们用Type这个字符串类型,让ActionTypes中的每一个类型一个个去过滤匹配,比如传入的是AddType:

代码语言:javascript
复制
PickPayload<ActionTypes, AddType>
复制代码

则会被分布成:

代码语言:javascript
复制
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已经确定,所以我们可以进一步的提前固定参数。(有点类似于函数柯里化)

代码语言:javascript
复制
type PickStorePayload<T> = PickPayload<ActionTypes, T>;
复制代码

此时,我们定义一个类型安全的Vuex实例所需要的所有辅助类型都定义完毕:

代码语言:javascript
复制
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>;
复制代码

使用起来就很简单了:

代码语言:javascript
复制
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…

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020年01月09日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 这篇文章适合谁:
  • 通过这篇文章,你可以学到以下特性在实战中是如何使用的:
  • 写在前面:
  • vuex骨架
  • dispatch的类型安全
  • action形参中payload的类型安全
  • 总结
  • 参考文章
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档