TypeScript 3.4 で Redux の Action をラクに書く

こんにちは、HRBrain 鈴木です 普段はReact + TypeScriptで社のサービス開発をしています! およそ2ヶ月前TypeScript 3.4 がリリースされました(もうすぐ 3.5 も出ます!)
少し時間が経ってしまいましたが、新機能の「const assertion」をつかって Redux の Action をちょっぴりラクに書く方法をご紹介します

const assertion とは

TypeScript には自分の型を定義する機能があります

const hoge = 'Hoge'
type Hoge = typeof hoge // -> 'Hoge' 型

上記の例では、一度変数に入れた後 typeof 使用して型を取り出すと string ではなく 'Hoge' 型になります しかし、この方法は弱点がありました hoge を別の変数に代入すると型の強制力が無くなって string になってしまいます

const hoge = 'Hoge'

const withHoge = (o: object) => ({ ...o, hoge: hoge })
// -> (typeof o) & { hoge: string }

ですので、これには型アノテーションを付ける必要があります

- const withHoge = (o: object) => ({ ...o, hoge: hoge })
// -> (typeof o) & { hoge: string }
+ const withHoge = <T extends object>(o: T): T & { hoge: typeof hoge } => ({ ...o, hoge: hoge })
// -> (typeof o) & { hoge: 'Hoge' }

これを一々書いていたら大変です そこで「const assertion」を使うと以下のように書けます

const hoge = 'Hoge' as const

const withHoge = (o: object) => ({ ...o, hoge: hoge })
// -> (typeof o) & 'Hoge'

再代入しても 'Hoge' 型を維持できるようになりました
これを応用して Redux の Action を書いてみましょう!

const assertion で Action を定義する

先程の例を応用して ActionTypes を作ってみます

const Increment = 'Increment' as const
const Decrement = 'Decrement' as const
const Replace = 'Replace' as const

「const assertion」を使用したので別の変数に代入しても型が失われないようになりました
次に Action を定義します

const increment = () => ({
  type: Increment
})
const decrement = () => ({
  type: Decrement
})
const replace = (payload: number) => ({
  type: Replace,
  payload
})

型アノテーションも無くシンプルに書けました 最後に Actions 型を作りましょう

type Actions = ReturnType<typeof increment | typeof decrement | typeof replace>
// -> { type: 'Increment' } | { type: 'Decrement' } | { type: 'Replace'; payload: number }

これもかなりシンプルです!
ReturnType を使って Action から型を推論させることでわざわざ Action 一つ一つの型を定義せずとも Union 型を作ることができます!

最終的に State やら Reducer やらを追加すると以下のようになります メチャメチャシンプルです

/**
 * State
 */

type State = {
    count: number
}

/**
 * Actions
 */

const Increment = 'Increment' as const
const Decrement = 'Decrement' as const
const Replace = 'Replace' as const

/**
 * Actions Creators
 */

const increment = () => ({
    type: Increment
})

const decrement = () => ({
    type: Decrement
})

const replace = (payload: number) => ({
  type: Replace,
  payload
})

type Actions = ReturnType<typeof increment | typeof decrement | typeof replace>

/**
 * Reducer
 */

const initialState: State = {
    count: 0
}

const reducer = (state: State | null, action: Actions):State => {
    if (!state) return initialState

    switch (action.type) {
        case Increment:
            return { count: state.count + 1 }

        case Decrement:
            return { count: state.count - 1 }

        case Replace:
            return { count: action.payload }

        default:
            throw new Error('Invalid action type')
    }
}

最後に

コードがシンプルだとテンション上がりますよね
HRBrainではそんな「Simple is more」なコードを一緒に書いてくれる人を募集しています!

www.wantedly.com