Cover image for typesafe-actionsを使って型安心なRedux Storeを実装する
reactreduxredux-observabletypescript

typesafe-actionsを使って型安心なRedux Storeを実装する

December 07, 2018

7 min read

mitsuruogMitsuru Ogawa

この記事はReact.js その 2 Advent Calendar 2018 8 日目の記事です。

半年くらい前に、React + Redux + redux-observable + TypeScript の実践的サンプルという記事を書いたのですが、Redux の action と reducer の部分の型があまりうまく定義できてなかったので、個人的  に課題だと感じていました。

今回はその部分をtypesafe-actionsを使って型安心に実装する方法の紹介です。

プロジェクト全体のコードはこちらを参照してください。

以前のコード

以前のコードは Action に interface を定義して、型が必要な Reducer などの部分でこの interface を使って Any 型をキャストする方法を取っていました。

// Actionでinterfaceを定義して
interface CoolAction {
  isSuperCool: boolean;
  payload: {};
}
// Reducerの中でキャストする
switch (action.type) {

  case COOL_ACTION:
    return Object.assign({}, state, {
      cool: new CoolModel((action as CoolAction).payload)
    });

  default:
    return state;
}

この場合、どうしても Action と Reducer の間で Any 型となってしまい、型安心とは言えない状態でした。 最近になってreact-redux-typescript-guideを読んでいたら、Redux Store 周りを型安心にする方法が載っていたので試してみました。

typesafe-actions を使って型安心な Redux Store を実装する

Action の定義

まず Action を定義します。以前を同じようにcreateActionをつかって型を定義していきます。 resolveの中に Action の payload を渡します。これが後に Reducer などで利用されます。

import { createAction } from "typesafe-actions";

import { WEATHER_GET } from "../constants";

export const weatherGetAction = createAction(
  WEATHER_GET,
  (resolve) => (lat: number, lng: number) => resolve({ lat, lng })
);
// これと同じ
// {
//   type: WEATHER_GET,
//   payload: { lat: number, lng: number },
// }

typesafe-actions にはactionというもう少し簡単に Action を定義できる関数がありますが、 この場合、getTypeisActionOfなどの Helper 関数と一緒に使えない Action となります。 個人的には、Helper 関数を使わない理由があまりないので、面倒でもcreateActionを使うことをおすすめします。

Reducer で Action の型を使う

次に Reducer で Action の型の使い型です。 ActionTypeのジェネリックに Action の定義を渡すと、Action の実行結果の型が返ってきます。これを Action の型として利用します。

import { ActionType, getType } from "typesafe-actions";

import * as actions from "../actions";

type Action = ActionType<typeof actions>;
// 実際のActionには複数のActionのUnion typeが設定されている
// {  type: WEATHER_GET, payload: { lat: number, lng: number } } |
// {  type: WEATHER_GET, payload: { weather: Response } }

ActionTypeの中身については非常に難しい部分ですが、簡単に説明すると TypeScript のReturnType<T>を使って型情報を取得しています。

ReturnType<T>Tに渡された関数を実行してその戻り値の型情報を取得するものです。

例えば、上の actions の中にweatherGetActionweatherSetActionがあった場合、 ActionType<typeof actions>の結果はweatherGetAction | weatherSetActionのような Union type になります。(実際にはweatherGetAction関数の戻り値の型です。)

次に Action の型情報を取得できたので、これを Reducer の Action の型定義として使います。 Type の判定はgetTypeを使うことで型安心に判定することができます。

export const weatherReducer = (state: WeatherState = initialState, action: Action): WeatherState => {

  switch (action.type) {

    case getType(actions.weatherSetAction):
      // このActionの型は { type: WEATHER_GET, payload: { lat: number, lng: number} }
      return Object.assign({}, state, { weather: new Weather(action.payload) });

    ...

    default:
      return state;
  }
};

以上が基本的な流れです。

おまけ、redux-observable と一緒に使う

typesafe-actions に redux-observable についての記載があるのですが、これだけだとうまく行かなかったので、うまく行かなかったポイントを紹介します。

Epic の実装

まず、Reducer と同じ方法で Action の型情報を取得しておきます。 Epic の実装は上の typesafe-actions のガイドの通りに実装すれば大丈夫です。isOfTypegetTypeを使って型安全に実装していきます。

// ActionはReducerと同じように取得したActionの型情報
export const weatherGetEpic: Epic<Action, Action, RootState> = (action$, store) =>
  action$.pipe(
    filter(isOfType(getType(actions.weatherGetAction))),
    switchMap(action =>
      ...
    )
  );

Store の実装

Store の実装の部分は少し変更が必要です。 次のように実装した場合、コンパイル時にエラーがでます。

const epicMiddleware = createEpicMiddleware();
// ちょー長いActionの型定義 is not assignable to parameter of type 'Epic<Action<any>, Action<any>, void, any>'.

Action と State の型情報がうまく渡っていないようなので、ジェネリックで型情報を渡すようにするとコンパイルできるようになります。

もしかすると自分の Epic の型定義がどこか間違っているだけかもしれないが。。。

const epicMiddleware = createEpicMiddleware<Action, Action, RootState>();

これで redux-observable と一緒に使っても型安心になりました。

まとめ

説明割愛してしまった部分もありますが、実際に動作しているコードはGitHubを見てください。

typesafe-actions で意外なハマりポイントがあったので、こちらにも目を通しておくといいです。