recomposeで見るTypeScriptの高階関数型推論


これはgotanda.js#11 用のLT資料です。

---

何の変哲もない React + ReduxをTypeScriptで書いた場合のコード

```tsx
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のアプリが書けるようになってる。

```ts
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を返す関数のこと。

```ts
import { ComponentType } from "react";
type HOC<P1, P2> = (c: ComponentType<P1>) => ComponentType<P2>;
```

react-reduxの`connect(...)`もHOC。

```tsx
const MyApp = connect(
  ({ count }: AppState) => ({ count }),
  (dispatch) => ({
    onIncrement: dispatch({ type: "INCREMENT" }),
    onDecrement: dispatch({ type: "DECREMENT" }),
  }),
)(MyComponent);
```

recompose自体も`withState`やら`lifecycle`やら色々なHOCを提供してくれるけど、そこは割愛。

---

複数のHOCを組み合わせるときは、`compose` で合成する

```ts
const enhancer = compose(hoc1, hoc2); // = (c) => hoc1(hoc2(c))
const EnhancedComponent = enhancer(BaseComponent);
```

たとえば、`{ hoge: string }` という値をpropsに注入するようなHOC `withHoge` があったとすると、

```ts
declare function withHoge<P extends { hoge?: string }>(c: ComponentType<P>): ComponentType<Omit<P, "hoge">>;
```

下記のように使う。実際、react-routerのwithRouterとかDIチックな動きをする系統のHOCはconnectと合わせて、container component側でcompositionするケースが多いと思う。

```ts
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を再度アノテートする必要があった、という意味。これは辛いぜ。

```ts
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](https://github.com/Microsoft/TypeScript/pull/30215) という推論強化が行われたことによって、下記のような汎関数の合成が行えるようになった。

```ts
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`できるようになる！


```ts
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の型定義が総称型使うようになってなかった :innocent:


---

- Higher order function type inferenceで汎関数合成がはかどるようになったよ
  - 実際のところ、この機能で恩恵受けるライブラリとかを色々考えてみたけども、「ジェネリクスが含まれる汎関数を返すような関数」の実例が、ReactのHOCとrecomposeくらいしか思い浮かばなかった。。。
  - なんか面白いサンプルあったら教えてください :pray:
- 余談ですが、TypeScript 3.5でこの機能が、「コンストラクタを合成する関数の結果の推論」まで強化されたので、こちらも面白いサンプルあれば。