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

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

然而靜態類型並沒有綁定 OOP。隨著 Redux 社區對 TS 的擁抱以及 TS 自身的發展,TS 對 FP 的表達能力勢必也會越來越強。Redux 社區也需要群策群力,為 TS 和 FP 的偉大結合做貢獻。

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

理論基礎

Mapped Types

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

下面介紹的 Mapped Types 讓 interface 更加強大。大家在 js 中都用過 map 運算。在 TS 中,interface 也能做 map 運算。

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

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

type NumberMap<T> = {}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 能夠通過一些方式獲取類型集的子類型。比如:

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 類型是 numberconst 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 優化

redux中原來的定義:

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> = {}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/red

讀者可以 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點:

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

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

const actions = { /** 加 */ add: ... /** 乘以 */ multiply: ...}

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

  • 2、每一個 actionCreator 需要定義 payload 類型。

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

const actions = { /** 加 */ add() { return { type: Types.add, payload: 3 }; }, /** 乘以 */ multiply: createAction<{ num: number }>(Types.multiply)}

  • 3、推導出可辨識聯合類型。

最後,還要能夠通過 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。

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


推薦閱讀:

推斷函數返回值的類型
Hello RxJS
有哪些公司在使用或者準備使用Angular2?
如何進一步熟悉甚至掌握Angular?
angular 和 typescript 到底是否適合最佳實踐?

TAG:前端开发 | Redux | TypeScript |