JavaScript
flow
React
flowtype
redux

Flowtype+reduxにおけるreducerの正しい型付け

よく以下のような型付けを見ます。

// @flow
import { Actions, type Action } from './actions'

export type State = {
  count: number,
  other: string,
}

const initialState: State = { count: 0, other: 'test' }

export default function(state: State = initialState, action: Action): State {
  switch (action.type) {
    case Actions.INCREMENT:
      return { ...state, counter: state.count + 1 }
    default:
      return state
  }
}

一見正しいように見えます。
flowの型エラーはでません。
しかし、よく見ると、countcounterとタイポしています。
これでは型安全などと言えませんね。

$Shape<T>を使った改善

この型のオブジェクトは、Tに記述されていないプロパティを含むことはできません。

type T = {
  foo: string,
  bar: number
}
const a: $Shape<T> = {
  foo: '121', // no errors about not having bar 
  baz: true   // error, baz is not in T type
}

これをreducerで使うと以下のようになります。
reducerの戻り値の型を$Shape<State>に変更します。

スクリーンショット 2017-09-16 17.18.05.png

グレート!
定義されてないcounterで型エラーが起きました。
countに直しましょう。

スクリーンショット 2017-09-16 17.19.55.png

エラーが消えました。
しかし、残念ながら、これではダメです。
定義したStateの型は以下でした。

type State = {
  count: number,
  other: string,
}

プロパティotherが消えているのに検知出来ていません。

T & $Shape<T> を使う

これを解決するには以下の型を定義します。

type Exact<T> = T & $Shape<T>

これは、Tのプロパティ全てかつそれ以外のプロパティを許容しない型です。

するとプロパティが足りない場合正しくflowが検知します。

スクリーンショット 2017-09-16 17.26.35.png

...stateを追加しましょう。
これで、reducerが型で守られるようになりました。

スクリーンショット 2017-09-16 17.54.36.png

全体では、以下のようになります。もちろんExactはreducerごとに定義するのではなくimport typeした方がよいでしょう。

スクリーンショット 2017-09-16 18.08.03.png

なぜ$Exact<T>ではダメなのか?

$Exact<T>だと全てのオブジェクトが正しくないとダメなので、{...state}を使うとその時点でエラーが起きてしまいます。
なので{ ...state, count: state.count + 1 }で型エラーが発生してしまいます。

スクリーンショット 2017-09-16 18.17.35.png

追記

{| |}$Exact<T>と同等です。

おわりに

上記のスクリーンショットのエディタはAtom-IDEを使っています。

Exact<T>ですが、それぞれのreducerに毎回import typeするか悩みますね。
どこでも使えるようにグローバルに定義してしまってもいいかもしれないとも思っています。
何かあればコメント欄またはtwitterにて議論しましょう。

関連記事

Flowtypeのactionの型付け。String Literal型とString型について
http://qiita.com/akameco/items/e7021e22da4c9e14463a