TypeScript基礎看膩了?進階實現智能類型推導的簡化版Vuex,手把手帶你實現。

以前幾篇講TypeScript的文章中,我帶來了在React中的一些小實踐html

React + TypeScript + Hook 帶你手把手打造類型安全的應用。vue

React Hook + TypeScript 手把手帶你打造use-watch自定義Hook,實現Vue中的watch功能。react

這篇文章我決定更進一步,直接用TypeScript實現一個類型安全的簡易版的Vuex。git

這篇文章適合誰:

  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的用法也是必須熟悉的。github

寫在前面:

本文實現的Vuex只有很簡單的stateactionsubscribeAction功能,由於Vuex當前的組織模式很是不適合類型推導(Vuex官方的type庫目前推斷的也很簡陋),因此本文中會有一些和官方不一致的地方,這些是刻意的爲了類型安全而作的,本文的主要目標是學習TypeScript,而不是學習Vuex,因此請小夥伴們不要嫌棄它代碼囉嗦或者和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構造函數定了兩個泛型SA,這是由於咱們須要推出stateaction的類型,因爲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的類型。

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的聯合類型。

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>();
複製代碼

目標達成:

type

payload

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給我返回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…

相關文章
相關標籤/搜索