reduxで面倒なactionの型まわりを楽する方法

Redux＋TSを書くときに個人的によくやるパターンを書いてみます

頑張って前日に書きました:sob:


#### はじめに

Reduxの流れ自体は単方向でグルグル回る感じになっています [画像引用元](https://stackoverflow.com/questions/45416237/axios-calls-in-actions-redux)

![redux_store_states.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131778/a16903f7-0da0-8803-a0c9-e778aae1aa71.png)



ReduxのTSに関しては単方向ではなく、Actionの型情報をComponentとReducerに渡す必要があり、ちょっと都合が違います。

<img width="560" alt="スクリーンショット 2019-12-22 16.53.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131778/e1022cf0-d805-48b8-26d3-edac5c2a0711.png">

これをある程度楽する方法を書いていきます

#### 書くもの概要
ユーザーの一覧の表示・追加・削除できる画面

雰囲気はこんなので
<img width="505" alt="スクリーンショット 2019-12-22 17.23.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131778/1c9273a1-b8d7-0567-6ed3-e9606bce2e9f.png">


以下ソース

##### model

```ts:モデル
// ユーザーのモデル
interface IUser {
  id: number
  name: string
  email: string
  age: number
}

```

##### Action

```ts:action
// アクションのTypeのあれ
export enum UserActions {
  ADD = 'user/ADD',
  REMOVE = 'user/REMOVE',
}

// アクションの一覧
export const userActions = {
  add: (payload: {user: User}) => ({
    type: UserActions.ADD as const,
    payload,
  }),
  remove: (payload: {userId: User['id']}) => ({
    type: UserActions.REMOVE as const,
    payload,
  }),
}

// 以下UtilityType

// MapをUnionとしてとりだす
type MapToUnion<T> = T[keyof T];

interface Action {
  type: string;
}

interface Actions {
  [key: string]: (...args: any) => Action;
}

// userActionsをUnionとして取り出すよう
type ActionToUnion<T extends Actions> = Extract<
  ReturnType<MapToUnion<T>>,
  Action
  >;

// userActionsを {[k: アクション名]: アクションの戻り値} にするやつ
type ActionToMap<T extends Actions> = { [P in keyof T]: ReturnType<T[P]> };

export type UserReducerAction = ActionToMap<typeof userActions>;
export type UserReducerActions = ActionToUnion<
  typeof userActions
  >;

```

##### Component

```tsx:component
interface mapStateToProps {
  users: User[]
}

interface dispatchProps {
  // ここで作った型を指定
  add: (payload: UserReducerAction['add']['payload']) => void
  remove: (payload: UserReducerAction['remove']['payload']) => void
}

type Props = mapStateToProps & dispatchProps

// コンポーネントに関しては特に書かない
const _UserPage: FC<Props> = ({users, addUser, removeUser}) => (
  <UserList
    // ユーザー一覧を受け取って表示
    users={users}
    // 作成時のイベント
    onCreate={(user: User) => addUser({user})}
    // 削除時のイベント
    onRemove={(user: User) => {removeUser({userId: user.id})}}
  />
)

// ストアのイメージ
interface Store {
  user: {
    users: User[]
  }
}

const mapStateToProps =(state: Store): mapStateToProps => ({
  users: state.user.users,
})


const mapDispatchToProps = (dispatch: Dispatch<UserReducerActions>): dispatchProps => ({
  addUser: payload => dispatch(userActions.addUser(payload)),
  removeUser: payload => dispatch(userActions.removeUser(payload)),
})
})

export const UserPage = connect(
  mapStateToProps,
  mapDispatchToProps
)(_UserPage);
```

##### Reducer

```ts:reducer
interface UserState {
  users: User[]
}

const initialState: UserState = {
  users: [],
}

// UserReducerActions これでActionの型を指定する
export const userReducer = (state = initialState, action: UserReducerActions): UserState => {
  switch (action.type) {
    case UserActions.ADD: {
      return {
        ...state,
        users: state.users.concat(action.payload.user)
      }
    }
    case UserActions.REMOVE: {
      const index = state.users.findIndex(v => v.id === action.payload.userId)
      if (index === -1) {
        return state
      }

      return {
        ...state,
        users: state.users.splice(index, 1);
      }
    }
    default:
      return state
  }
}


```

#### 解説

肝は 
- アクションをオブジェクトにまとめる https://qiita.com/eretica/private/81e8567756a0f98a3393#action
- `ActionToMap` と `ActionToUnion` を利用して扱いやすい型にする
- 以降はComponentやReducerで適宜利用する
- *redux-thunk* や *redux-saga* などでもconnectの部分以外は同じようなパターンで利用できる。使う際は `dispatch(userActions.add())` で利用可能


UtilyTypeで使いやすい形にすることで、楽できるよ！ってのが伝わると嬉しい

```ts
interface dispatchProps {
  addUser: (payload: UserReducerAction['add']['payload']) => void
  removeUser: (payload: UserReducerAction['remove']['payload']) => void
}
```

#### よりみち

`payload: Action['hoge']['payload']` が冗長であれば

```ts
interface dispatchProps {
  addUser: (payload: UserReducerAction['add']['payload']) => void
  removeUser: (payload: UserReducerAction['remove']['payload']) => void
}
```

こういうUtilityTypeをつくればもう少し楽できます

```ts

type ArgumentTypes<F extends Function> = F extends (args: infer A) => any ? (arg: A) => void : never;
type ActionToArguments<T extends Actions> = { [P in keyof T]: ArgumentTypes<T[P]>};
export type UserReducerActionArgument = ActionToArguments<typeof userActions>;

interface dispatchProps {
  addUser: UserReducerActionArgument['add']
  removeUser: UserReducerActionArgument['remove']
}
```

#### まとめ

- actionを使いやすい型にしておくことで、楽をしたい


#### よりみち2

こちらの記事([なぜカスタムフックを作るのか](https://qiita.com/saitoeku3/items/35dbf4eedd544ebde89c#redux--context-%E3%81%A8%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%82%92%E7%B9%8B%E3%81%90%E3%81%9F%E3%82%81
))のように、custom hooks が状態やアクションを提供する形になればコネクトの部分の型定義は不要になるので、さらに楽できるのではないかと思っています。

個人的にはこのようなcustom hooksはcontainerコンポーネントに相当するものだけが利用(use***)し、従来通りのコンポーネント設計に留めて置くのが吉なのかなとは思っています