你所不知道的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> = {
[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 能夠通過一些方式獲取型別集的子型別。比如:
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 供讀者體驗:
https://github.com/jasonHzq/redux-ts-helper
讀者可以 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。
FP 就像一匹脫韁的野馬,請用型別拴住它。
相關文章
- 你所不知道的 Typescript 與 Redux 型別優化TypeScriptRedux型別優化
- 你所不知道的Java效能優化之String!Java優化
- 你所不知道的前端效能優化不完全手冊前端優化
- 型別—-《你不知道的js》型別JS
- 你不知道的js型別轉化和原型鏈JS型別原型
- 你所不知道的cssCSS
- 你所不知道的 POST
- 你所不知道的 Transformer!ORM
- 你不知道的 TypeScript 泛型(萬字長文,建議收藏)TypeScript泛型
- TypeScript this型別TypeScript型別
- 你不知道的js讀後感-型別JS型別
- Typescript型別推斷技巧你知道麼?TypeScript型別
- 你所不知道的JavaScript(三)JavaScript
- 你所不知道的XML安全XML
- 你所不知道的JavaScript 二JavaScript
- 你所不知道的 CSS 陰影技巧與細節CSS
- TypeScript 泛型型別TypeScript泛型型別
- 你不知道的JavaScript--Item4 基本型別和基本包裝型別(引用型別)JavaScript型別
- 【譯】TypeScript 的型別(一)TypeScript型別
- 你所不知道的Python | 字串格式化的演進之路Python字串格式化
- Typescript:基本型別TypeScript型別
- TypeScript Widened型別TypeScriptIDE型別
- TypeScript Never型別TypeScript型別
- TypeScript Any型別TypeScript型別
- TypeScript 索引型別TypeScript索引型別
- TypeScript 型別相容TypeScript型別
- TypeScript 交叉型別TypeScript型別
- TypeScript void 型別TypeScript型別
- TypeScript 字串型別TypeScript字串型別
- TypeScript 型別安全TypeScript型別
- 你所不知道的 AI 進展AI
- SACC 2018:大牛眼中你所不知道的新零售技術架構與選型架構
- TypeScript 強大的型別別名TypeScript型別
- TypeScript type 型別別名TypeScript型別
- TypeScript 基本型別和泛型的使用TypeScript型別泛型
- 提升----你所不知道的JavaScript系列(3)JavaScript
- Python: 你所不知道的星號 * 用法Python
- TypeScript 條件型別精讀與實踐TypeScript型別