search
LoginSignup
20

More than 3 years have passed since last update.

posted at

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

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

by Quramy
1 / 11

これは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 で作った戻り値の汎関数における P1P3 がともに { } に落ちてしまっていた。

ジェネリクスを含む汎関数を返すような関数について、戻り値の型推論にてジェネリクスが保存されるようになった ということ。


これでようやくアノテート無しで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の型定義が総称型使うようになってなかった :innocent:


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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
20