型安全な createReducer を求めて & 宣伝

この記事は [2019 年の React Native Advent Calendar](https://qiita.com/advent-calendar/2019/react-native) 25 日目の記事です。

## TL;DR

React Native のデータフローは Redux が扱いやすいが、 `switch` 形式の Reducer だと計算量が最悪 O(n) になってしまうため、 テーブル参照形式を採用して O(1) としたい。[このテクニックは Redux 公式で紹介されている](https://redux.js.org/recipes/reducing-boilerplate#generating-reducers)が、これを TypeScript で使いたい。その実装例を紹介する。

というようなことを考えるきっかけとなったのは React Native の本の執筆をしているから。来年春までに出る予定。

## React Native のデータフロー

クリーンアーキテクチャー的にデータとその振る舞いを Entity や Usecase として閉じ込めておくにあたり、現状で Redux が最適だな、と感じています。 React State や `useReducer` は UI や Framework によりすぎていて、スマートな UI を助長するものなので必要最低限の使用に留めるのがよいでしょう。

「実装スピードが…」という方は[こちらのスライド](https://speakerdeck.com/twada/quality-and-speed)を読んでみてください。

## `switch` 版 Reducer の問題点

`switch` はただの `if` 文の連なりを少し見通しよく書けるというだけです。すべての `case` が上から順にマッチするまで判定されます。次のふたつは等価です。

```js
switch (action.type) {
  case ADD:
    break;
  case SUB:
    break;
  default:
    break;
}
```

```js
if (action.type === ADD) {
} else if (action.type === SUB) {
} else {
}
```

これだと Action Type が増えていくとマッチまで余計な判定が増えちゃうよね、しかも毎回、というのがざっくり問題になるわけです。まあ実用上そんなに問題にならないんですが、高速化できるところはしたいというモチベーション。

## 解決策

Action Type だけの判定なんだからキーに Action Type 、値にそれぞれの Action の reduce 処理を持った Object を定義してやれば `handlers[action.type](state, action)` みたいに呼び出せるじゃん、というのがアイディアです。

で、それを JavaScript で定義した場合のコードが次です。[公式](https://redux.js.org/recipes/reducing-boilerplate/#generating-reducers)から引っ張ってきています。

```js
function createReducer(initialState, handlers) {
  return function reducer(state = initialState, action) {
    if (handlers.hasOwnProperty(action.type)) {
      return handlers[action.type](state, action)
    } else {
      return state
    }
  }
}
```

わかりやすいですね。

## 型付けしよう

React Native も TypeScript 対応して長いので、じゃあ TypeScript で同様の関数を書こう、となるじゃないですか。が、一筋縄では行かないんですよこれが。

とりあえず眠いので現時点の到達点をどーん。

```typescript
import * as Redux from 'redux'

type Handler<State, Action> = (state: State, action: Action) => State

type Handlers<State, Actions> = {
  [Type in keyof Actions]: Handler<State, Actions[Type]>
}

interface TypeActionSet {
  [key: string]: any
}

export default function createReducer<
  State,
  Actions extends TypeActionSet,
  HandlerDefinitions extends Handlers<State, Actions> = Handlers<State, Actions>
>(initialState: State, handlers: HandlerDefinitions): Redux.Reducer<State> {
  return function(state = initialState, action): State {
    return handlers.hasOwnProperty(action.type)
      ? (handlers[action.type] as Handler<State, Actions[typeof action.type]>)(
          state,
          action as Actions[typeof action.type],
        )
      : state
  }
}
```

Action Type に Symbol を使うのはあまり旨味がないので文字列だけに限定しています。

[`any` を使っているので警察に逮捕されてしまいますね](https://qiita.com/Takepepe/items/3353159894ed57b6f0a8)。さらに `as` でキャストしているのでお行儀の良いコードではありません。

また、こう使う感じになるのでちょっと大変です。

```typescript
type State = ReturnType<typeof createInitialState>
interface TypeActionSet {
  [ADD]: ReturnType<typeof add>
  [SUB]: ReturnType<typeof sub>
}


export default createReducer<State, TypeActionSet>(createInitialState(), {
  [ADD]: (state, action) => state + action.payload.plus,
  [SUB]: (state, action) => state - action.payload.minus,
}
```

課題として次が残っています。

1. `any` 撲滅
2. キャスト撲滅
3. TypeActionSet

Action は `type` プロパティが存在すること以外の規定がないので、実質 `any` と同様です。つまり 1, 2 は撲滅が難しそうです。また、 3 は Action の型から Action Type と Action の組の型を導出することができるのか？ということになります。 TypeScript Compiler API を使えば自動で吐き出すことはできそうですが…。

これで良ければ適度に使ってみてください。

## ちなみに

本家が出している [Redux Toolkit](https://redux-toolkit.js.org/) で `createReducer` として同様のものが提供されていますが、 Reducer に渡ってくる `action` が `any` となっていて例えば次のコードを書いても怒ってくれません。

```typescript
export default createReducer<State, TypeActionSet>(createInitialState(), {
  [ADD]: (state, action) => state + action.payload.plus,
  // 指定するプロパティ名を間違えてしまった！
  [SUB]: (state, action) => state - action.payload.plus,
}
```

コレジャナイ感がすごかったので、自前で考えることにしたのでした。

## 宣伝

さて、なんでこんなことを考えたのかと言うと、本を書いていて、 Redux の章を僕が担当したからです。執筆陣は @YutamaKotaro 、 @nitaking 、僕です。技術評論社さんから出版していただく予定です。

対象読者はもちろん React Native でアプリを作りたい方です。次のスキルセットがあれば読める本としています。

- JavaScript(ECMASCript 5)
- HTML
- CSS
- Git

目次は次の予定ですが、まだ変動する可能性があります。

1. React.js / React Nativeの概要とその背景
2. TypeScriptとECMAScript 2015
3. 開発環境の構築
4. React Nativeの基本
5. 作成するアプリケーションの仕様策定
6. テストによる設計の質の向上
7. Navigationの概要と実装
8. Atomic Designとコンポーネントの実装
9. データフローの設計および実装
10. FireBaseを使ったバックエンド連携
11. E2Eテスト
12. アプリストアへの公開

Lean Core 構想も軌道に乗り React Hooks も馴染みつつある今、まあいい頃合いだよね、いっちょ書いてみっか、という感じです。 3 月初旬出版を目指して執筆陣、編集者さんともに頑張っていますので乞うご期待。

このニュースがみなさんへのクリスマスプレゼントになるよう祈っています。メリークリスマス！