原發於知乎專欄,歡迎關注:zhuanlan.zhihu.com/p/32112508javascript
自從 Redux
誕生後,函數式編程在前端一直很熱;去年7月,Typescript
發佈 2.0,OOP 數據流框架也開始火熱,社區更傾向於類型友好、沒有 Redux 那麼冗長煩瑣的 Mobx 和 dob。html
然而靜態類型並無綁定 OOP。隨着 Redux 社區對 TS 的擁抱以及 TS 自身的發展,TS 對 FP 的表達能力勢必也會愈來愈強。Redux 社區也須要羣策羣力,爲 TS 和 FP 的偉大結合作貢獻。前端
本文主要介紹 Typescript
一些有意思的高級特性;並用這些特性對 Redux 作了類型優化,例如:推導全局的 Redux State 類型、Reducer 每一個 case 下拿到不一樣的 payload 類型;Redux 去形式化與 Typescript 的結合;最後介紹了一些 React 中經常使用的 Typescript 技巧。java
在 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
,官方文檔已有詳細講解,本文再也不贅述。連接以下:
可辨識聯合是什麼,我只引用官方文檔代碼片斷作快速介紹:
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 屬性的類型,它是一個字符串字面量類型。
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 供讀者體驗:
讀者能夠 clone 下來在 vscode 中進行體驗。
用 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 的去形式化工具,所以本文再也不贅述 Redux Action
的去形式化,只說 Redux Action 的類型優化。
筆者總結以下3點:
例如,能夠定義定一個字面量對象來存儲 actionCreators。
const actions = {
/** 加 */
add: ...
/** 乘以 */
multiply: ...
}
複製代碼
一方面其它模塊引用起來會很方便,一方面能夠對字面量作批量類型推導。而且其中的註釋,只有在這種字面量下,纔可以在 vscode 中解析,以在其它模塊引用時能夠提升辨識度,提升開發體驗。
以下代碼所示,不管 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.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)}>
複製代碼
ReactRoute 提供了 RouteComponentProps 類型,提供了 location、params 的類型定義
type Props = OriginProps & RouteComponentProps<Params, {}>
複製代碼
通常來講,先後端之間會用一個 API 約定平臺或者接口約定文檔,來作先後端解耦,好比 rap、 swagger。筆者在團隊中作了一個把接口約定轉換成 Typescript 類型定義代碼的。通過筆者團隊的實踐,這種工具對開發效率、維護性都有很大的提升。
接口類型定義對開發的幫助:
在可維護性上。例如,一旦接口約定進行更改,API 的類型定義代碼會從新生成,Typescript 可以檢測到字段的不匹配,前端便能快速修正代碼。最重要的是,因爲前端代碼與接口約定的綁定關係,保證了接口約定文檔具備百分百的可靠性。咱們得以經過接口約定來構建一個可靠的測試系統,進行自動化的聯調與測試。
把 interface 全部屬性變成可選:
interface Obj {
a: number;
b: string;
}
type OptionalObj = Partial<Obj>
// interface OptionalObj {
// a?: number;
// b?: string;
// }
複製代碼
把 interface 全部屬性變成 readonly:
interface Obj {
a: number;
b: string;
}
type ReadonlyObj = Readonly<Obj>
// interface ReadonlyObj {
// readonly a: number;
// readonly b: string;
// }
複製代碼
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 就像一匹脫繮的野馬,請用類型拴住它。