reduxとflowtypeを組み合わせたサンプルはreduxのサンプルにあるが、redux-thunkとの組み合わせの例はない。この記事では、reduxとflowtypeのサンプルを示したあと、redux-thunkで型の恩恵を受ける方法を紹介する。
redux公式のflowtypeサンプル
redux/examples/todos-flow at bc3f2aeb669f4ad5e424f6a711fd588e9bd3462e · reactjs/redux
types以下はこうなっている。
// @flow
import type { Store as ReduxStore, Dispatch as ReduxDispatch } from 'redux'
export type Id = number;
export type Text = string;
export type Todo = {
  id: Id,
  text: Text,
  completed: boolean
};
export type VisibilityFilter =
    'SHOW_ALL'
  | 'SHOW_ACTIVE'
  | 'SHOW_COMPLETED'
  ;
export type Todos = Array<Todo>;
export type State = {
  todos: Todos,
  visibilityFilter: VisibilityFilter
};
export type Action =
    { type: 'ADD_TODO', id: Id, text: Text }
  | { type: 'TOGGLE_TODO', id: Id }
  | { type: 'SET_VISIBILITY_FILTER', filter: VisibilityFilter }
  ;
export type Store = ReduxStore<State, Action>;
export type Dispatch = ReduxDispatch<Action>;
reduxのstoreとdispatchの定義使いまわすことで型定義の恩恵を得られる。
actionやreducerでの使い方はサンプルのレポジトリを見てほしい。
ここで使われている型定義はflow-typedと同等であり、以下のような型定義である。
flow-typed/react-redux_v4.x.x.js at c4bbd91cfc455a5e061baf9e36a0e0a631eeb4b6 · flowtype/flow-typed
declare module 'redux' {
  /*
    S = State
    A = Action
  */
  declare type Dispatch<A: { type: $Subtype<string> }> = (action: A) => A;
  declare type MiddlewareAPI<S, A> = {
    dispatch: Dispatch<A>;
    getState(): S;
  };
  declare type Store<S, A> = {
    // rewrite MiddlewareAPI members in order to get nicer error messages (intersections produce long messages)
    dispatch: Dispatch<A>;
    getState(): S;
    subscribe(listener: () => void): () => void;
    replaceReducer(nextReducer: Reducer<S, A>): void
  };
  declare type Reducer<S, A> = (state: S, action: A) => S;
  declare type Middleware<S, A> =
    (api: MiddlewareAPI<S, A>) =>
      (next: Dispatch<A>) => Dispatch<A>;
  declare type StoreCreator<S, A> = {
    (reducer: Reducer<S, A>, enhancer?: StoreEnhancer<S, A>): Store<S, A>;
    (reducer: Reducer<S, A>, preloadedState: S, enhancer?: StoreEnhancer<S, A>): Store<S, A>;
  };
  declare type StoreEnhancer<S, A> = (next: StoreCreator<S, A>) => StoreCreator<S, A>;
  declare function createStore<S, A>(reducer: Reducer<S, A>, enhancer?: StoreEnhancer<S, A>): Store<S, A>;
  declare function createStore<S, A>(reducer: Reducer<S, A>, preloadedState: S, enhancer?: StoreEnhancer<S, A>): Store<S, A>;
  declare function applyMiddleware<S, A>(...middlewares: Array<Middleware<S, A>>): StoreEnhancer<S, A>;
  declare type ActionCreator<A, B> = (...args: Array<B>) => A;
  declare type ActionCreators<K, A> = { [key: K]: ActionCreator<A, any> };
  declare function bindActionCreators<A, C: ActionCreator<A, any>>(actionCreator: C, dispatch: Dispatch<A>): C;
  declare function bindActionCreators<A, K, C: ActionCreators<K, A>>(actionCreators: C, dispatch: Dispatch<A>): C;
  declare function combineReducers<O: Object, A>(reducers: O): Reducer<$ObjMap<O, <S>(r: Reducer<S, any>) => S>, A>;
  declare function compose<S, A>(...fns: Array<StoreEnhancer<S, A>>): Function;
}
しかし、redux-thunkを導入すると整合性が取れなくなる。
見てわかると思うが、DispatchがActionしか取らないからだ。
redux-thunk使用時の型定義
redux-thunkとflowtype環境で使う場合は、以下のような型定義を使うといい。
declare module 'redux' {
  /*
    S = State
    A = Action
  */
  declare type ThunkAction<S, R> = (dispatch: Dispatch<S, any>, getState: () => S) => R;
  declare type ThunkDispatch<S> = <R>(action: ThunkAction<S, R>) => R;
  declare type PlainDispatch<A: {type: $Subtype<string>}> = (action: A) => A;
  declare type Dispatch<S, A> = PlainDispatch<A> & ThunkDispatch<S>
  declare type MiddlewareAPI<S, A> = {
    dispatch: Dispatch<S, A>;
    getState(): S;
  };
  declare type Store<S, A> = {
    // rewrite MiddlewareAPI members in order to get nicer error messages (intersections produce long messages)
    dispatch: Dispatch<S, A>;
    getState(): S;
    subscribe(listener: () => void): () => void;
    replaceReducer(nextReducer: Reducer<S, A>): void
  };
  declare type Reducer<S, A> = (state: S, action: A) => S;
  declare type Middleware<S, A> =
    (api: MiddlewareAPI<S, A>) =>
      (next: Dispatch<S, A>) => Dispatch<S, A>;
  declare type StoreCreator<S, A> = {
    (reducer: Reducer<S, A>, enhancer?: StoreEnhancer<S, A>): Store<S, A>;
    (reducer: Reducer<S, A>, preloadedState: S, enhancer?: StoreEnhancer<S, A>): Store<S, A>;
  };
  declare type StoreEnhancer<S, A> = (next: StoreCreator<S, A>) => StoreCreator<S, A>;
  declare function createStore<S, A>(reducer: Reducer<S, A>, enhancer?: StoreEnhancer<S, A>): Store<S, A>;
  declare function createStore<S, A>(reducer: Reducer<S, A>, preloadedState: S, enhancer?: StoreEnhancer<S, A>): Store<S, A>;
  declare function applyMiddleware<S, A>(...middlewares: Array<Middleware<S, A>>): StoreEnhancer<S, A>;
  declare type ActionCreator<A, B> = (...args: Array<B>) => A;
  declare type ActionCreators<K, A> = { [key: K]: ActionCreator<A, any> };
  declare function bindActionCreators<S, A, C: ActionCreator<A, any>>(actionCreator: C, dispatch: Dispatch<S, A>): C;
  declare function bindActionCreators<S, A, K, C: ActionCreators<K, A>>(actionCreators: C, dispatch: Dispatch<S, A>): C;
  declare function combineReducers<O: Object, A>(reducers: O): Reducer<$ObjMap<O, <S>(r: Reducer<S, any>) => S>, A>;
  declare function compose<S, A>(...fns: Array<StoreEnhancer<S, A>>): Function;
}
上書きの仕方がちょっと変わって、以下のようになる。
import type {Store as ReduxStore, Dispatch as ReduxDispatch} from 'redux';
// 略
export type Dispatch = ReduxDispatch<State, Action>;
export type Store = ReduxStore<State, Action>;
これでredux-thunk環境でもflowtypeの恩恵を受けられるようになる。
参考
redux/examples/todos-flow at bc3f2aeb669f4ad5e424f6a711fd588e9bd3462e · reactjs/redux
flow-typed/react-redux_v4.x.x.js at c4bbd91cfc455a5e061baf9e36a0e0a631eeb4b6 · flowtype/flow-typed
This gist shows how to add support for redux-thunk and redux-promise-middleware to the flowtype libdefs for redux