你所不知道的 Typescript 與 Redux 類型優化

原發於知乎專欄,歡迎關注:zhuanlan.zhihu.com/p/32112508javascript

自從 Redux 誕生後,函數式編程在前端一直很熱;去年7月,Typescript 發佈 2.0,OOP 數據流框架也開始火熱,社區更傾向於類型友好、沒有 Redux 那麼冗長煩瑣的 Mobx 和 dobhtml

然而靜態類型並無綁定 OOP。隨着 Redux 社區對 TS 的擁抱以及 TS 自身的發展,TS 對 FP 的表達能力勢必也會愈來愈強。Redux 社區也須要羣策羣力,爲 TS 和 FP 的偉大結合作貢獻。前端

本文主要介紹 Typescript 一些有意思的高級特性;並用這些特性對 Redux 作了類型優化,例如:推導全局的 Redux State 類型、Reducer 每一個 case 下拿到不一樣的 payload 類型;Redux 去形式化與 Typescript 的結合;最後介紹了一些 React 中經常使用的 Typescript 技巧。java

理論基礎

Mapped Types

Javascript 中,字面量對象和數組是很是強大靈活。引進類型後,如何避免由於類型的約束而使字面量對象和數組死氣沉沉,Typescript 靈活的 interface 是一個偉大的發明。git

下面介紹的 Mapped Types 讓 interface 更增強大。你們在 js 中都用過 map 運算。在 TS 中,interface 也能作 map 運算。github

// 將每一個屬性變成可選的。
type Optional<T> = {
 [key in keyof T]?: T[key];
}
複製代碼

從字面量對象值推導出 interface 類型,並作 map 運算:typescript

type NumberMap<T> = {
  [key in keyof T]: number;
}

function toNumber<T>(obj: T): NumberMap<T> {
  return Object.keys(obj).reduce((result, key) => {
    return {
      ...result,
      [key]: Number(result[key]),
    };
  }, {}) as any;
}

const obj2 = toNumber({
  a: '32',
  b: '64',
});
複製代碼

在 interface map 運算的支持下,obj2 能推導出精準的類型。編程

獲取函數返回值類型

在 TS 中,有些類型是一個類型集,好比 interface,function。TS 可以經過一些方式獲取類型集的子類型。好比:redux

interface Person {
  name: string;
}

// 獲取子類型
const personName: Person['name'];
複製代碼

然而,對於函數子類型,TS 暫時沒有直接的支持。不過江湖上有一種類型推斷的方法,能夠獲取返回值類型。後端

雖然該方法能夠說又繞又不夠優雅,可是函數返回值類型的推導,可以更好地支持函數式編程,收益遠大於成本。

type Reverse<T> = (arg: any) => T;

function returnResultType<T>(arg: Reverse<T>): T {
  return {} as any as T;
}

// result 類型是 number
const result = returnResultType((arg: any) => 3);
type ResultType = typeof result;
複製代碼

舉個例子,當咱們在寫 React-redux connect 的時候,返回結構極有可能與 state 結構不盡相同。而經過推導函數返回類型的方法,能夠拿到準確的返回值類型:

type MapProps<NewState> = (state?: GlobalState, ownProps?: any) => NewState;
function returnType<NewState>(mapStateToProps: MapProps<NewState>) {
  return {} as any as NewState;
}
複製代碼

使用方法:

function mapStateToProps(state?: GlobalState, ownProp?: any) {
  return {
    ...state.dataSrc,
    a: '',
  };
};

const mockNewState = returnType(mapStateToProps);
type NewState = typeof mockNewState;
複製代碼

可辨識聯合(Discriminated Unions)

關於 Discriminated Unions ,官方文檔已有詳細講解,本文再也不贅述。連接以下:

查看英文文檔

查看中文文檔

可辨識聯合是什麼,我只引用官方文檔代碼片斷作快速介紹:

interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}

type Shape = Square | Rectangle;

function area(s: Shape) {
    switch (s.kind) {
        // 在此 case 中,變量 s 的類型爲 Square
        case "square": return s.size * s.size;
        // 在此 case 中,變量 s 的類型爲 Rectangle
        case "rectangle": return s.height * s.width;
    }
}
複製代碼

在不一樣的 case 下,變量 s 可以擁有不一樣的類型。我想讀者一會兒就聯想到 Reducer 函數了吧。注意 interface 中定義的 kind 屬性的類型,它是一個字符串字面量類型。

redux 類型優化

combineReducer 優化

原來的定義:

type Reducer<S> = (state: S, action: any) => S;

function combineReducers<S>(reducers: ReducersMapObject): Reducer<S>; 複製代碼

粗看這個定義,好似沒有問題。但熟悉 Redux 的讀者都知道,該定義忽略了 ReducersMapObject 和 S 的邏輯關係,S 的結構是由 ReducersMapObject 的結構決定的。

以下所示,先用 Mapped Types 拿到 ReducersMapObject 的結構,而後用獲取函數返回值類型的方法拿到子 State 的類型,最後拼成一個大 State 類型。

type Reducer<S> = (state: S, action: any) => S;

type ReducersMap<FullState> = {
  [key in keyof FullState]: Reducer<FullState[key]>;
}

function combineReducers<FullState>(reducersMap: ReducersMap<FullState>): Reducer<FullState>; 複製代碼

使用新的 combineReducers 類型覆蓋原先的類型定義後,通過 combineReducers 的層層遞歸,最終能夠經過 RootReducer 推導出 Redux 全局 State 的類型!這樣在 Redux Thunk 中和 connect 中,能夠享受全局 State 類型,不再須要懼怕寫錯局部 state 路徑了!

拿到全局 State 類型:

function returnType<FullState>(reducersMap: ReducersMap<FullState>): FullState {
  return ({} as any) as FullState;
}

const mockGlobalState = returnType(RootReducer);

type GlobalState = typeof mockGlobalState;
type GetState = () => GlobalState;
複製代碼

去形式化 & 類型推導

Redux 社區一直有不少去形式化的工具。可是如今風口不同了,去形式化多了一項重大任務,作好類型支持!

關於類型和去形式化,因爲 Redux ActionCreator 的型別取決於實際項目使用的 Redux 異步中間件。所以本文拋開筆者自身業務場景,只談方法論,只作最簡單的 ActionCreator 解決方案。讀者能夠用這些方法論建立適合本身項目的類型系統。

經團隊同窗提醒,爲了讀者有更好的類型體感,筆者建立了一個 repo 供讀者體驗:

github.com/jasonHzq/re…

讀者能夠 clone 下來在 vscode 中進行體驗。

Redux Type

enum 來聲明 Redux Type ,能夠說是最精簡的了。

enum BasicTypes {
  changeInputValue,
  toggleDialogVisible,
}

const Types = createTypes(prefix, BasicTypes);
複製代碼

而後用 createTypes 函數修正 enum 的類型和值。

createTypes 的定義以下所示,一方面用 Proxy 對屬性值進行修正。另外一方面用 Mapped Types 對類型進行修正。

type ReturnTypes<EnumTypes> = {
    [key in keyof EnumTypes]: key;
}

function createTypes<EnumTypes>(prefix, enumTypes: EnumTypes): ReturnTypes<EnumTypes> {
    return new Proxy(enumTypes as any, {
        get(target, property: any) {
            return prefix + '/' + property;
        }
    })
}
複製代碼

讀者請注意,ReturnTypes 中,Redux Type 類型被修正爲一個字符串字面量類型(key)!覺得創造一個可辨識聯合作準備。

Redux Action 類型優化

市面上有不少 Redux 的去形式化工具,所以本文再也不贅述 Redux Action 的去形式化,只說 Redux Action 的類型優化。

筆者總結以下3點:

  • 一、要有一個總體 ActionCreators 的 interface 類型。

例如,能夠定義定一個字面量對象來存儲 actionCreators。

const actions = {
  /** 加 */
  add: ...
  /** 乘以 */
  multiply: ...
}
複製代碼

一方面其它模塊引用起來會很方便,一方面能夠對字面量作批量類型推導。而且其中的註釋,只有在這種字面量下,纔可以在 vscode 中解析,以在其它模塊引用時能夠提升辨識度,提升開發體驗。

  • 二、每個 actionCreator 須要定義 payload 類型。

以下代碼所示,不管 actionCreator 是如何建立的,其 payload 類型必須明確指定。以便在 Reducer 中享用 payload 類型。

const actions = {
  /** 加 */
  add() {
    return { type: Types.add, payload: 3 };
  },
  /** 乘以 */
  multiply: createAction<{ num: number }>(Types.multiply)
}
複製代碼
  • 三、推導出可辨識聯合類型。

最後,還要可以經過 actions 推導出可辨識聯合類型。如此才能在 Reducer 不一樣 case 下享用不一樣的 payload 類型。

須要推導出的 ActionType 結構以下:

type ActionType = { type: 'add', payload: number }
  | { type: 'multiply', payload: { num: number } };
複製代碼

推導過程以下:

type ActionCreatorMap<ActionMap> = {
  [key in keyof ActionMap]: (payload?, arg2?, arg3?, arg4?) => ActionMap[key]
};
type ValueOf<ActionMap> = ActionMap[keyof ActionMap];

function returnType<ActionMap>(actions: ActionCreatorMap<ActionMap>) {
  type Action = ValueOf<ActionMap>;

  return {} as any as Action;
}

const mockAction = returnType(actions);
type ActionType = typeof mockAction;

function reducer(state: State, action: ActionType): State {
  switch (action.type) {
    case Types.add: { return ... }
    case Types.muliple: { return ... }
  }
}
複製代碼

前端類型優化

經常使用的React類型

  • Event

React 中 Event 參數很常見,所以 React 提供了豐富的關於 Event 的類型。好比最經常使用的 React.ChangeEvent:

// HTMLInputElement 爲觸發 Event 的元素類型
handleChange(e: React.ChangeEvent<HTMLInputElement>) {
  // e.target.value
  // e.stopPropagation
}
複製代碼

筆者更喜歡把 Event 轉換成對應的 value

function pipeEvent<Element = HTMLInputElement>(func: any) {
  return (event: React.ChangeEvent<HTMLInputElement>) => {
    return func(event.target.value, event);
  };
}

<input onChange={pipeEvent(actions.changeValue)}>
複製代碼
  • RouteComponentProps

ReactRoute 提供了 RouteComponentProps 類型,提供了 location、params 的類型定義

type Props = OriginProps & RouteComponentProps<Params, {}>
複製代碼

自動產生接口類型

通常來講,先後端之間會用一個 API 約定平臺或者接口約定文檔,來作先後端解耦,好比 rap、 swagger。筆者在團隊中作了一個把接口約定轉換成 Typescript 類型定義代碼的。通過筆者團隊的實踐,這種工具對開發效率、維護性都有很大的提升。

接口類型定義對開發的幫助:

在可維護性上。例如,一旦接口約定進行更改,API 的類型定義代碼會從新生成,Typescript 可以檢測到字段的不匹配,前端便能快速修正代碼。最重要的是,因爲前端代碼與接口約定的綁定關係,保證了接口約定文檔具備百分百的可靠性。咱們得以經過接口約定來構建一個可靠的測試系統,進行自動化的聯調與測試。

經常使用的默認類型

  • Partial

把 interface 全部屬性變成可選:

interface Obj {
  a: number;
  b: string;
}

type OptionalObj = Partial<Obj>

// interface OptionalObj {
// a?: number;
// b?: string;
// }
複製代碼
  • Readonly

把 interface 全部屬性變成 readonly:

interface Obj {
  a: number;
  b: string;
}

type ReadonlyObj = Readonly<Obj>

// interface ReadonlyObj {
// readonly a: number;
// readonly b: string;
// }
複製代碼
  • Pick
interface T {
  a: string;
  b: number;
  c: boolean;
}

type OnlyAB = Pick<T, 'a' | 'b'>;

// interface OnlyAB {
// a: string;
// b: number;
// }
複製代碼

總結

在 FP 中,函數就像一個個管道,在管道的鏈接處的數據塊的類型老是不盡相同。下一層管道使用類型每每須要從新定義。

可是若是有一個肯定的推導函數返回值類型的方法,那麼只須要知道管道最開始的數據塊類型,那麼全部管道鏈接處的類型均可以推導出來。

當前 TS 版本尚不支持直接獲取函數返回值類型,雖然本文介紹的間接方法也能解決問題,但最好仍是但願 TS 早日直接支持:issue

FP 就像一匹脫繮的野馬,請用類型拴住它。

相關文章
相關標籤/搜索