これはgotanda.js#11 用のLT資料です。
何の変哲もない React + ReduxをTypeScriptで書いた場合のコード
import React, { ComponentType } from "react";
import { Action, Dispatch, combineReducers, createStore } from "redux";
import { connect, Provide } from "react-redux";
import { render } from "react-dom";
// component
type Props = {
count: number,
onIncrement: () => void,
onDecrement: () => void,
someOuterProp: string,
};
const MyComponent: ComponentType<Props> = ({ someOuterProp, count, onIncrement, onDecrement }) => (
<div>
count: {count}
<button onClick={onIncrement}>+1</button>
<button onClick={onDecrement}>-1</button>
</div>
);
type IncrementAction = { type: "INCREMENT" };
type DecrementAction = { type: "DECREMENT" };
type AppActions = IncrementAction | DecrementAction;
const count = (state: number = 0, action: AppActions) => {
switch (action.type) {
default:
return state;
case "INCREMENT":
return state + 1;
case "DECREMENT":
return state - 1;
}
};
const reducer = combineReducers({ count });
type AppState = ReturnType<typeof reducer>;
const MyApp = connect(
({ count }: AppState) => ({ count }),
(dispatch) => ({
onIncrement: dispatch({ type: "INCREMENT" }),
onDecrement: dispatch({ type: "DECREMENT" }),
}),
)(MyComponent);
const store = createStore(
combineReducers({ count }),
);
render(
<Provide store={store}>
<MyApp someOuterProp="gotanda" />
</Provide>
, document.getElementById("app"));
比較的最近(2.8以降くらい)のTypeScriptであれば、必要最小限の型アノテーションだけでreduxのアプリが書けるようになってる。
type IncrementAction = { type: "INCREMENT" };
type DecrementAction = { type: "DECREMENT" };
type AppActions = IncrementAction | DecrementAction;
const count = (state: number = 0, action: AppActions) => {
switch (action.type) {
default:
return state;
case "INCREMENT":
return state + 1;
case "DECREMENT":
return state - 1;
}
};
const reducer = combineReducers({ count });
type AppState = ReturnType<typeof reducer>; // => { count: number }
ところでrecomposeって:
- https://github.com/acdlite/recompose
- ReactにおけるHigher Order Component(HOC)関連のユーティリティライブラリ
- React hooksの登場と共にメンテ終了のお知らせ
HOCはReact Componentを引数にとって、React Componentを返す関数のこと。
import { ComponentType } from "react";
type HOC<P1, P2> = (c: ComponentType<P1>) => ComponentType<P2>;
react-reduxのconnect(...)
もHOC。
const MyApp = connect(
({ count }: AppState) => ({ count }),
(dispatch) => ({
onIncrement: dispatch({ type: "INCREMENT" }),
onDecrement: dispatch({ type: "DECREMENT" }),
}),
)(MyComponent);
recompose自体もwithState
やらlifecycle
やら色々なHOCを提供してくれるけど、そこは割愛。
複数のHOCを組み合わせるときは、compose
で合成する
const enhancer = compose(hoc1, hoc2); // = (c) => hoc1(hoc2(c))
const EnhancedComponent = enhancer(BaseComponent);
たとえば、{ hoge: string }
という値をpropsに注入するようなHOC withHoge
があったとすると、
declare function withHoge<P extends { hoge?: string }>(c: ComponentType<P>): ComponentType<Omit<P, "hoge">>;
下記のように使う。実際、react-routerのwithRouterとかDIチックな動きをする系統のHOCはconnectと合わせて、container component側でcompositionするケースが多いと思う。
const enhancer = compose(
connect(
({ count }: AppState) => ({ count }),
(dispatch) => ({
onIncrement: dispatch({ type: "INCREMENT" }),
onDecrement: dispatch({ type: "DECREMENT" }),
}),
),
withHoge,
);
const MyApp = enhancer(MyComponent);
ようやく今日の本題。3.3以前のTypeScriptでは、compose
で合成されたHOCからはpropsの型情報が抜け落ちてしまう問題があった。
composeする際に、自分でHOCのinとoutのpropsを再度アノテートする必要があった、という意味。これは辛いぜ。
const enhancer = compose<{
count: number,
onIncrement: () => void,
onDecrement: () => void,
hoge: string,
someOuterProp: string,
}, {
someOuterProp: string,
}>(
connect(
({ count }: AppState) => ({ count }),
(dispatch) => ({
onIncrement: dispatch({ type: "INCREMENT" }),
onDecrement: dispatch({ type: "DECREMENT" }),
}),
),
withHoge,
);
TypeScriptの3.4で Higher order function type inference という推論強化が行われたことによって、下記のような汎関数の合成が行えるようになった。
type HOC<P1, P2> = (c: ComponentType<P1>) => ComponentType<P2>;
function compose<P1, P2, P3>(hoc12: HOC<P1, P2>, hoc23: HOC<P2, P3>): HOC<P1, P3>;
3.3までは、compose
で作った戻り値の汎関数における P1
、 P3
がともに { }
に落ちてしまっていた。
ジェネリクスを含む汎関数を返すような関数について、戻り値の型推論にてジェネリクスが保存されるようになった ということ。
これでようやくアノテート無しでcompose
できるようになる!
const enhancer = compose(
connect(
({ count }: AppState) => ({ count }),
(dispatch) => ({
onIncrement: dispatch({ type: "INCREMENT" }),
onDecrement: dispatch({ type: "DECREMENT" }),
}),
),
withHoge,
);
const MyApp = enhancer(MyComponent);
、、、と言うには実はまだ早くて、DefinitelyTypedみたらrecomposeのd.tsの型定義が総称型使うようになってなかった
- Higher order function type inferenceで汎関数合成がはかどるようになったよ
- 実際のところ、この機能で恩恵受けるライブラリとかを色々考えてみたけども、「ジェネリクスが含まれる汎関数を返すような関数」の実例が、ReactのHOCとrecomposeくらいしか思い浮かばなかった。。。
- なんか面白いサンプルあったら教えてください
- 余談ですが、TypeScript 3.5でこの機能が、「コンストラクタを合成する関数の結果の推論」まで強化されたので、こちらも面白いサンプルあれば。