Redux typed actions でReducerを型安全に書く (TypeScriptのバージョン別)

# はじめに
Redux + TypeScriptな環境でAction typeに型を定義し、Reducerを型安全に書く方法をTypeScriptのバージョン 1.8 / 2.0 / 2.1 別にまとめてみた。

なお、試したサンプルコードは [GitHub](https://github.com/wadahiro/react-redux-typescript-boilerplate) にあります。下記の通りTypeScriptのバージョン別にブランチで分けてあります。

* [TypeScript 1.8版](https://github.com/wadahiro/react-redux-typescript-boilerplate/tree/typescript-1.8)
* [TypeScript 2.0版](https://github.com/wadahiro/react-redux-typescript-boilerplate/tree/typescript-2.0)
* [TypeScript 2.1版](https://github.com/wadahiro/react-redux-typescript-boilerplate/tree/typescript-2.1)

また、TypeScript 2.0、2.1はまだリリースされていないので、8/12時点の物を使って確認している(2.0は`npm i typescript@beta`で、2.1は`npm i typescript@next`でインストール)。

## サンプルの内容

![sampleApp.png](https://qiita-image-store.s3.amazonaws.com/0/35233/c5bfae07-cf53-b866-5746-375832dbf4a6.png)


* `+`ボタンをクリックするとカウンタを1つインクリメント
* `-`ボタンをクリックするとカウンタを1つデクリメント
* テキストフィールドを直接編集することで任意の値にカウンタを設定

というシンプルなアプリ。ReduxのActionは下記の3つを使う。プレーンなJavaScriptのObjectで定義する。


* インクリメントのアクション

```js
{
    type: 'INCREMENT'
}
```
* デクリメントのアクション

```js
{
    type: 'DECREMENT'
}
```
* 任意の値にカウンタを設定するアクション

```js
{
    type: 'SET_COUNT',
    payload: {
        count: 100
    }
}
```

以下、TypeScriptのバージョン別にAction、Reducerのコードを解説。

# 1.8の場合

1.8だと2.0から導入される[Tagged union types](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#tagged-union-types) が使えないので、[User-defined type guard functions](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#user-defined-type-guard-functions)を定義して頑張る方法がある。

## Action
* Action typeはstringとして扱えるように`type ActionType<T> = string & _ActionType<T>`のように定義した`ActionType<T>`型で扱う。`T`型には各Actionの型を指定する。
* `isType`のように[User-defined type guard functions](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#user-defined-type-guard-functions)として型を絞るための共通関数を用意しておく。この関数で、先の`ActionType<T>`の`T`型、つまり各Actionの型に絞ることができるようにする。

```ts:Action
export interface _ActionType<T> { }
export type ActionType<T> = string & _ActionType<T>

export interface Action {
    type: String;
    payload?: any;
}

export function isType<T extends Action>(action: Action, type: _ActionType<T>): action is T {
    return action.type === type;
}

export const INCREMENT: ActionType<IncrementAction> = 'INCREMENT';
export const DECREMENT: ActionType<DecrementAction> = 'DECREMENT';
export const SET_COUNT: ActionType<SetCounterAction> = 'SET_COUNT';

export interface IncrementAction extends Action {
}
export function increment(): IncrementAction {
    return {
        type: INCREMENT
    };
}

export interface DecrementAction extends Action {
}
export function decrement(): DecrementAction {
    return {
        type: DECREMENT
    };
}

export interface SetCounterAction extends Action {
    payload: {
        count: number;
    }
}
export function setCount(num: number): SetCounterAction {
    return {
        type: SET_COUNT,
        payload: {
            count: num
        }
    };
}
```

## Reducer
* Actionで定義した`isType`関数を使って`action`変数の型を絞ることができる。
* Reduxのサンプルで良くあるような`switch case`を使った形にはできない。

```ts:Reducer
export const appStateReducer = (state: AppState = init(), action: Actions.Action) => {
    if (Actions.isType(action, Actions.INCREMENT)) {
        return Object.assign({}, state, {
            count: state.count + 1
        });
    }

    if (Actions.isType(action, Actions.DECREMENT)) {
        return Object.assign({}, state, {
            count: state.count - 1
        });
    }

    if (Actions.isType(action, Actions.SET_COUNT)) {
        return Object.assign({}, state, {
            count: action.payload.count
        });
    }

    return state;
};
```

[User-defined type guard functions](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#user-defined-type-guard-functions)により型が絞られるので、下記のようにちゃんと`action`変数の型を認識してくれている。存在しないプロパティにアクセスすればもちろんコンパイルエラーとなる。

![ts180.png](https://qiita-image-store.s3.amazonaws.com/0/35233/effb9f0c-4465-3b0f-ac5d-d98283626f19.png)


なお、このやり方はReduxのIssue: [Support for typed actions (Typescript) #992](https://github.com/reactjs/redux/issues/992) で紹介されていたものです。他にもClassを使った方法もここで紹介されているが、ActionごとにClassを定義するのはやり過ぎな気がするのでこのやり方がシンプルかなと思っている。


# 2.0の場合

2.0だと[Tagged union types](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#tagged-union-types)という機能が追加されており、[String Literal](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#string-literal-types)で型定義した`type`でActionの型を判別し絞り込むことができるようになる。

## 一部微妙な書き方
書き方はシンプルで良いのだが型安全でないところがある書き方をまず紹介。

### Action
* 1.8の頃と違い、素直に[String Literal](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#string-literal-types)で`type`を定義するだけと非常にシンプル。

```ts:Action
import { Action } from 'redux';

export type Actions = IncrementAction | DecrementAction | SetCounterAction;

export interface IncrementAction extends Action {
    type: 'INCREMENT';
}
export function increment(): IncrementAction {
    return {
        type: 'INCREMENT'
    };
}

export interface DecrementAction extends Action {
    type: 'DECREMENT';
}
export function decrement(): DecrementAction {
    return {
        type: 'DECREMENT'
    };
}

export interface SetCounterAction extends Action {
    type: 'SET_COUNT';
    payload: {
        count: number;
    }
}
export function setCount(num: number): SetCounterAction {
    return {
        type: 'SET_COUNT',
        payload: {
            count: num
        }
    };
}
```

### Reducer
* 素直に[String Literal](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#string-literal-types)で`switch case`を書く。こちらもシンプル。

```ts:Reducer
export const appStateReducer = (state: AppState = init(), action: Actions.Actions) => {
    switch (action.type) {

        case 'INCREMENT':
            return Object.assign({}, state, {
                count: state.count + 1
            });

        case 'DECREMENT':
            return Object.assign({}, state, {
                count: state.count + -1
            });

        case 'SET_COUNT':
            return Object.assign({}, state, {
                count: action.payload.count
            });
    }

    return state;
};
```

これで型の絞り込みは行われる。例えば以下のように、`case 'SET_COUNT':` 内ではちゃんと`action`変数の型が絞られていることが分かる。

![ts200-narrow.png](https://qiita-image-store.s3.amazonaws.com/0/35233/9ee276c4-634c-618f-c295-e74b7d1ad164.png)


しかし残念な点もある。以下のように、`case 'DECREMENT':` -> `case 'AAAAA':` のように存在しないAction typeを書いたとしても、コンパイルエラーとして検知してくれず型安全ではない。

![ts200.png](https://qiita-image-store.s3.amazonaws.com/0/35233/15c34092-5722-cce9-8086-ecedf5199be8.png)


## コンパイルエラーを出させるためにちょっと工夫したやり方
アイデアとしては、[String Literal](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#string-literal-types)を直接書かずに変数で`case`を書けるようにすれば、タイプミスしてもコンパイルエラーとして検知される。

### Action
* 直接[String Literal](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#string-literal-types)で`type`を定義はせず、`const INCREMENT: 'INCREMENT' = 'INCREMENT';` のように[String Literal](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#string-literal-types)の定数を宣言しておく。
* その定数を `type: typeof INCREMENT;` のように使い、`type`の型を[String Literal](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#string-literal-types)として定義する。

```ts:Action
import { Action } from 'redux'

export type Actions = IncrementAction | DecrementAction | SetCounterAction;

export const INCREMENT: 'INCREMENT' = 'INCREMENT';
export const DECREMENT: 'DECREMENT' = 'DECREMENT';
export const SET_COUNT: 'SET_COUNT' = 'SET_COUNT';

export interface IncrementAction extends Action {
    type: typeof INCREMENT;
}
export function increment(): IncrementAction {
    return {
        type: INCREMENT
    };
}

export interface DecrementAction extends Action {
    type: typeof DECREMENT;
}
export function decrement(): DecrementAction {
    return {
        type: DECREMENT
    };
}

export interface SetCounterAction extends Action {
    type: typeof SET_COUNT;
    payload: {
        count: number;
    }
}
export function setCount(num: number): SetCounterAction {
    return {
        type: SET_COUNT,
        payload: {
            count: num
        }
    };
}
```

### Reducer
* Actionで定義した定数を使って`case`を指定する。
* 直接[String Literal](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#string-literal-types)を入力しないので、タイプミスした場合もこれならコンパイルエラーとして検知できる。
* もちろん、型の絞り込みも行われる。

```ts:Reducer
export const appStateReducer = (state: AppState = init(), action: Actions.Actions) => {
    switch (action.type) {

        case Actions.INCREMENT:
            return Object.assign({}, state, {
                count: state.count + 1
            });

        case Actions.DECREMENT:
            return Object.assign({}, state, {
                count: state.count + -1
            });

        case Actions.SET_COUNT:
            return Object.assign({}, state, {
                count: action.payload.count
            });
    }

    return state;
};
```

# 2.1の場合
2.1だと[Tagged union types](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#tagged-union-types)がさらに改善されているようで、2.0の時のような微妙な工夫も必要なくなる。つまり、2.0で紹介した一部駄目なやり方でも型安全となる。全く同じコードだが再掲する。

## Action
* 素直に[String Literal](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#string-literal-types)で`type`を定義する。

```ts:Action
import { Action } from 'redux';

export type Actions = IncrementAction | DecrementAction | SetCounterAction;

export interface IncrementAction extends Action {
    type: 'INCREMENT';
}
export function increment(): IncrementAction {
    return {
        type: 'INCREMENT'
    };
}

export interface DecrementAction extends Action {
    type: 'DECREMENT';
}
export function decrement(): DecrementAction {
    return {
        type: 'DECREMENT'
    };
}

export interface SetCounterAction extends Action {
    type: 'SET_COUNT';
    payload: {
        count: number;
    }
}
export function setCount(num: number): SetCounterAction {
    return {
        type: 'SET_COUNT',
        payload: {
            count: num
        }
    };
}
```

## Reducer
* 素直に[String Literal](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#string-literal-types)で`switch case`を書く。

```ts:Reducer
export const appStateReducer = (state: AppState = init(), action: Actions.Actions) => {
    switch (action.type) {

        case 'INCREMENT':
            return Object.assign({}, state, {
                count: state.count + 1
            });

        case 'DECREMENT':
            return Object.assign({}, state, {
                count: state.count + -1
            });

        case 'SET_COUNT':
            return Object.assign({}, state, {
                count: action.payload.count
            });
    }

    return state;
};
```

2.0と異なり、`action.type`に存在しない[String Literal](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#string-literal-types)を`switch case`に書くとちゃんとコンパイルエラーとして検出される。便利。

![ts210.png](https://qiita-image-store.s3.amazonaws.com/0/35233/134f292d-4a02-fbbc-8269-3590473525cf.png)

# まとめ
* TypeScriptを使うとReduxのReducerを型安全に書けるようになるのでいいよ。1.8でもちょっと面倒だができなくはない。
* TypeScript 2 の[Tagged union types](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#tagged-union-types)では簡単に書けるようになるよ。
* ただし2.0だと不十分で、型の絞り込みはしてくれるが[String Literal](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#string-literal-types)のAction typeをタイプミスしてしまうとコンパイルで検知してくれず。検知させるには別途変数を定義しておく必要がありちょっと面倒。
* TypeScript 2.1ならここは改善されていて、[String Literal](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#string-literal-types)のままでいけて良い感じ！

というわけではやく2.1がリリースされて欲しい...