4ヶ月ほど前、 Flow & Redux で Reducer の実装パターンを考える という記事を書いたが、 reducer を型付けするという目的は達しているものの、明らかに記述量が増えて冗長だという問題があった。
ところでこういう記事を見つけた。
これを flow 向けに書き直した、というかセマンティクスが一緒だったので、そのまま動いたのだが、これを自分向けに書き直したらとてもいい感じになった.
flow は v0.54.1
コード
// @flow
type __ReturnType<B, F: (...any) => B> = B
type $ReturnType<F> = __ReturnType<*, F>
// Actions
const INCREMENT = 'counter/increment'
const ADD = 'counter/add'
export function increment() {
return { type: INCREMENT }
}
export function add(n: number) {
return { type: ADD, payload: n }
}
export type Action =
| $ReturnType<typeof increment>
| $ReturnType<typeof add>
// Reducer
export type State = {
value: number
}
const initialState = {
value: 0
}
export default (state: State = initialState, action: Action) => {
switch (action.type) {
case INCREMENT: {
action.payload // ここがエラーになる!!!
return { value: state.value + 1 }
}
case ADD: {
return { value: state.value + action.payload }
}
default: {
return state
}
}
}
INCREMENT のときは action.payload が type refinement によって正しくアクセスエラーになり、 冒頭の Hackey なおまじないと Action 列挙以外はほとんど冗長な部分が消えた。以下解説。
関数の型の返り値の型を返す型
最初は何を言ってるかわからないだろうが、関数の型を与えるとその返り値を取り出す型を定義する。
type __ReturnType<B, F: (...any) => B> = B
type $ReturnType<F> = __ReturnType<*, F>
これを使って ActionCreator から Actionを取り出して その Union Type とする。
export type Action =
| $ReturnType<typeof increment>
| $ReturnType<typeof add>
これだけ。冗長な型定義はもういらない。
実際はどこかの util にこの型を書いておけば良さそう。
改良案1: 定数を Symbol にする
同名の定数を定義してreducerが誤爆してデバッグに苦しんだことがあるのは僕だけじゃないと思う。
ES2015のSymbolを使う。関数を呼ぶ度にユニークな参照を生成する。uuid みたいに使える。
// Actions
const INCREMENT = Symbol()
const ADD = Symbol()
冗長な記述が減り、ちゃんとflowの推論機は追ってくれる。偉い。
問題は、 redux-logger とか仕込んでもtype名が判明せずログが読みにくかったり、 SymbolはJSONシリアライズできないので、なんらかの都合で log をサーバーに送ろうとしても type 属性が消滅することだろうか。 reducer 内部で完結するなら問題ない。
改良案2: redux-promise 対応
自分は複雑なmiddlewareが嫌いでredux-promiseを使うことが多いのだが、そうなると ActionCreator の 返り値が Action | Promise<Action>
になるので、そこからも $ReturnType で推論できるようにする。
type __ReturnType<B, F: (...any) => B | Promise<B>> = B
type $ReturnType<F> = __ReturnType<*, F>
他の middleware もこうやって対応できる
ゴール
// @flow
import type { $ReturnType } form '../types'
// Actions
const INCREMENT = Symbol()
const ADD = Symbol()
export const increment = () => ({ type: INCREMENT })
export const add = (n: number) => ({ type: ADD, payload: n })
export type Action =
| $ReturnType<typeof increment>
| $ReturnType<typeof add>
// Reducer
export type State = {
value: number
}
const initialState = {
value: 0
}
export default (state: State = initialState, action: Action) => {
switch (action.type) {
case INCREMENT: {
return { value: state.value + 1 }
}
case ADD: {
return { value: state.value + action.payload }
}
default: {
return state
}
}
}
展望
Stage:0 Patter Matching が来たら reducer もっとよく書けそう