LoginSignup
38
26

More than 3 years have passed since last update.

【redux】as constでaction creatorからActionの型を簡単につくる

Last updated at Posted at 2019-04-21

3.4以前では、型付をするのにas assertionでLiteralTypeを使う必要があり面倒でした。が、TS3.4から登場したas constによってreduxの型付はとても手軽になりました。

まずActionCreatorを作ります。このときtype属性にas constを付けるよりオブジェクトにas constを付ける方がすべての属性がreadonlyになるのでオススメします。

const increment = (payload: number) =>
  ({
    type: 'increment',
    payload,
  } as const)

Action型を作るのは以下のような型定義が必要になります。やっていることは単純でそれぞれのActionCreator から ReturnTypeで戻り値の型を取り出しそのUnion型を作ります。AnyAction は必須ではないですが、 typeプロパティを必ず含んでいることを保証することができます。

import { AnyAction } from 'redux'

export type ActionsType<ActionCreators extends object> = {
  [Key in keyof ActionCreators]: ActionCreators[Key] extends (
    ...args: any[]
  ) => AnyAction
    ? ReturnType<ActionCreators[Key]>
    : never
}

export type ActionType<
  ActionCreators extends object,
  Actions = ActionsType<ActionCreators>
> = { [Key in keyof Actions]: Actions[Key] }[keyof Actions]

使い方としては、as const付きでActionCreatorを作成し、ActionType<typeof actions>でAction型を作成できます。

actions.ts
export const increment = (payload: number) =>
  ({
    type: 'increment',
    payload,
  } as const)

export const decrement = (payload: number) =>
  ({
    type: 'decrement',
    payload,
  } as const)

export const typo = (payload: string) =>
  ({
    // typo...
    typo: 'typo',
    payload,
  } as const)
types.ts
import { ActionType } from './redux-actions-type'
import * as actions from './actions'

type Action = ActionType<typeof actions>
/**
 * type Action = {
 *   type: "increment";
 *   payload: number;
 * } | {
 *   type: "decrement";
 *   payload: number;
 * }
 */

typeプロパティだけでなく、payloadが正しく推論されていることがわかります。
また、typeプロパティがないものはnever型になるので、Action型には現れることはないので安心して使えます。

あとは、普通にreducerなどで使うだけです。

reducer.ts
import { Action } from './types'

type State = {
  count: number
}

export const reducer = (state: State = { count: 0 }, action: Action): State => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + action.payload }
    case 'decrement':
      // 主題とはずれるが戻り値のStateを省略するとこの余計な`hoge`プロパティを検知できない。
      // reduxのReducer型を使っても同じように検知されないので注意
      // return { count: state.count - action.payload, hoge: 1 }
      return { count: state.count - action.payload }
    // Type Error...
    // case 'typo':
    //   return { count: action.payload }
    default:
      return state
  }
}

また、Actionはreducerごとに別々の型にするのではなく、プロジェクトで一つの型にした方が取り回しが楽です。

毎回この定義を書くは面倒なので、ライブラリからインストールできます。(スターするのはよい選択肢です🤭)
akameco/redux-actions-type: easy typing of redux action

$ npm install redux-actions-type
or
$ yarn add redux-actions-type
types.ts
import { ActionType } from 'redux-actions-type'
import * as actions from './actions'

export type Action = ActionType<typeof actions>

また付属でActionsという型をexportできます。
これは個別のAction型を取れるので使う必要があれば(あまりそういうシーンはありませんが)使うことができます。たいていはActionCreater自身のReturnTypeで済むでしょう。

import { ActionsType } from 'redux-actions-type'
import * as actions from './actions'

export type Actions = ActionsType<typeof actions>
/**
 * type IncrementAction = {
 *   type: "increment";
 *   payload: number;
 * }
 */
type IncrementAction = Actions['increment']

TypeScriptの型非常に便利でいいですね。これからの進化にも期待です。

38
26
2

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
  3. You can use dark theme
What you can do with signing up
38
26