【React, redux, redux-saga】typesafe-actionsでコード量を減らそう

# はじめに
reduxのAction周りの型をしっかり付けようとすると、どうしてもコード量が多くなってしまいます。
さらに、redux-sagaを使って副作用を実装する場合は、副作用ごとに `request`、`success`、`failure`の3ケースについてActionとActionCreatorを定義する必要があるため大変です。
この悩みを解決するために、本稿では[typesafe-actions](https://github.com/piotrwitek/typesafe-actions)というライブラリを使って少ないコードでしっかり型をつける方法を紹介します。

## 題材にするコード
今回はYouTube Data APIを利用して、動画を検索できるクライアントアプリを題材に説明していきます。
コードは[こちら](https://github.com/YTakahashii/my-youtube-client/tree/qiita-typesafe-actions)

# 実装内容
今回は入力されたテキストから動画を検索する機能をredux-sagaを用いて実装していきます。
これをredux-sagaのフローに分けると以下のようになります。

1. 検索ボックスにテキストを入力してsubmitしたらAPIを叩く（REQUEST）
2. 正常に値を受け取れた場合、Storeに保存する（SUCCESS）
3. APIとの通信に失敗した場合、エラーを投げる（FAILURE）

[![Image from Gyazo](https://i.gyazo.com/e01acdef145648f418ea1fcf39989a92.gif)](https://gyazo.com/e01acdef145648f418ea1fcf39989a92)

## ActionType, ActionCreator, Actionの実装
まずはActionTypeを定義します。

```actions/Video/VideoActionType.ts
enum VideoActionType {
  SEARCH_VIDEOS_REQUEST = "SEARCH_VIDEOS_REQUEST",
  SEARCH_VIDEOS_SUCCEEDED = "SEARCH_VIDEOS_SUCCEEDED",
  SEARCH_VIDEOS_FAILED = "SEARCH_VIDEOS_FAILED",
}

export default VideoActionType;
```

続いて、ActionCreatorを実装します。ここで、`typesafe-actions`の出番です。
`createAsyncAction`を利用することで、簡単にActionCreatorを定義できます。

```actions/Video/VideoActionCreator.ts
import VideoActionType from './VideoActionType';
import SearchListResponse from '../../states/SearchListResponse';
import { createAsyncAction } from 'typesafe-actions';

export const searchVideos = createAsyncAction(
  VideoActionType.SEARCH_VIDEOS_REQUEST,
  VideoActionType.SEARCH_VIDEOS_SUCCEEDED,
  VideoActionType.SEARCH_VIDEOS_FAILED
)<string, SearchListResponse, Error>();
```

以下の3つのActionCreatorをまとめて生成することができました。
-  `searchVideos.request(payload: string)`
-  `searchVideos.success(payload: SearchListResponse)`
-  `searchVideos.success(payload: Error)`

それぞれのメソッドは、引数で指定した`type`とジェネリクスで指定した型の`payload`を返します。`payload`がいらない場合はジェネリクスの該当部分に`undefined`を指定しましょう。

素のTypeScriptだけで実装する場合、以下のように1つずつ定義する必要があるので、だいぶ楽ですね。

```no-typesafe-actions.ts
function request(payload: string) {
  return {
    type: VideoActionType.SEARCH_VIDEOS_REQUEST,
    payload,
  };
}

function success(payload: SearchListResponse) {
  return {
    type: VideoActionType.SEARCH_VIDEOS_SUCCEEDED,
    payload,
  };
}

function failure(payload: Error) {
  return {
    type: VideoActionType.SEARCH_VIDEOS_FAILED,
    payload,
  };
}
```

最後にActionCreatorの戻り値のActionオブジェクトも一つの型にまとめましょう。
ここでは`typesafe-actions`の`ActionType`を使うことで`searchVideos`が持つ3つのActionCreatorの戻り値のActionオブジェクトを1つのunion型にまとめることができます。

この型は、Reducerの引数`action: VideoAction`やcontainerのmapDispatchToPropsの引数`dispatch: Distatch<VideoAction>`のように使用します。

```actions/Video/VideoAction.ts
import { ActionType } from "typesafe-actions";
import * as ActionCreators from "./VideoActionCreator";

type VideoAction = ActionType<typeof ActionCreators>;

export default VideoAction;
```

## sagaの実装
`searchVideosSaga`は`searchVideos.request(payload: string)`が呼び出されると起動するジェネレータ関数です。この部分は最後の行の`takeLatest`で設定しています。引数でrequestの戻り値のActionオブジェクトを受け取ります。

`action`の型の指定はTypeScriptの`ReturnType`を使います。今回の場合`action: PayloadAction<VideoActionType.SEARCH_VIDEOS_REQUEST, string>`ですが、ActionCreatorの引数が無い場合の型は`EmptyAction<...>`になるため、直接指定するのはナンセンスだからです。

さらに、`yield`の戻り値は型が`any`になってしまうため、`yield call()`で呼び出す`api.search`の戻り値の型を自分で指定する必要があります。

`api.search`の型は、`YouTubeApi.search(q: string): Promise<SearchListResponse | undefined>`なため、`ReturnType`で`Promise`以下を取り出し、さらに下記の`PromiseGenericType`で`Promise`のジェネリクス部分の型（`SearchListResponse | undefined`）を取り出しています。

```sagas/Video/VideoSaga.ts
export function* searchVideosSaga(
  action: ReturnType<typeof VideoActionCreators.searchVideos.request>
) {
  const searchListResponse: PromiseGenericType<ReturnType<typeof api.search>> = yield call(
    api.search,
    action.payload
  );

  if (searchListResponse) {
    yield put(VideoActionCreators.searchVideos.success(searchListResponse));
  } else {
    yield put(
      VideoActionCreators.searchVideos.failure(new Error('searchListResponse is undefined'))
    );
  }
}

export const videoSagas = [takeLatest(VideoActionType.SEARCH_VIDEOS_REQUEST, searchVideosSaga)];
```

```utils/TypeUtils.ts
export type PromiseGenericType<T> = T extends Promise<infer U> ? U : T;
```

## その他
### Redux storeのRootStateの定義
`StateType`を使うことでreducerからRoot Stateの型を定義することができます。
手動でRootStateを定義するとreducerを更新するたびに変更しなければいけないため、バグに繋がってしまうので、これを使うのがおすすめです。

```state/index.ts
import { StateType } from 'typesafe-actions';
import rootReducer from 'src/reducers';

type RootState = StateType<typeof rootReducer>;
export default RootState;
```

`RootState`はcontainerで以下のように使うと型推論が効いてミスを無くすことができます。

```containers/VideoList.ts
import RootState from 'src/states';
import VideoList from 'src/components/VideoList/VideoList';
import { VideoListConnectedProps } from 'src/components/VideoList/VideoListProps';
import { connect } from 'react-redux';

const mapStateToProps = (state: RootState): VideoListConnectedProps => ({
  searchListResponse: state.searchListResponse,
});

export default connect(
  mapStateToProps,
  null
)(VideoList);

```

### 
# おわりに
`typesafe-actions`には他にも便利なユーティリティがたくさんあるので、ぜひ使ってみてください。