よく以下のような型付けを見ます。
// @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の型エラーはでません。
しかし、よく見ると、countをcounterとタイポしています。
これでは型安全などと言えませんね。
$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>
に変更します。
グレート!
定義されてないcounterで型エラーが起きました。
countに直しましょう。
エラーが消えました。
しかし、残念ながら、これではダメです。
定義したStateの型は以下でした。
type State = {
count: number,
other: string,
}
プロパティother
が消えているのに検知出来ていません。
T & $Shape<T> を使う
これを解決するには以下の型を定義します。
type Exact<T> = T & $Shape<T>
これは、Tのプロパティ全てかつそれ以外のプロパティを許容しない型です。
するとプロパティが足りない場合正しくflowが検知します。
...state
を追加しましょう。
これで、reducerが型で守られるようになりました。
全体では、以下のようになります。もちろんExactはreducerごとに定義するのではなくimport typeした方がよいでしょう。
なぜ$Exact<T>ではダメなのか?
$Exact<T>だと全てのオブジェクトが正しくないとダメなので、{...state}
を使うとその時点でエラーが起きてしまいます。
なので{ ...state, count: state.count + 1 }
で型エラーが発生してしまいます。
追記
{| |}
は$Exact<T>
と同等です。
おわりに
上記のスクリーンショットのエディタはAtom-IDEを使っています。
Exact<T>
ですが、それぞれのreducer
に毎回import type
するか悩みますね。
どこでも使えるようにグローバルに定義してしまってもいいかもしれないとも思っています。
何かあればコメント欄またはtwitterにて議論しましょう。
この型定義便利なのでflowのUtility Typesの仲間に入れてほしい。$Exact<T>と$Shape<T>の中間的な働きをする pic.twitter.com/2DK4JA6w7A
— 無職.js (@akameco) September 16, 2017
関連記事
Flowtypeのactionの型付け。String Literal型とString型について
http://qiita.com/akameco/items/e7021e22da4c9e14463a