JavaScript
flow
flowtype
redux
redux-thunk

flowtypeでredux-thunkの型定義を使う

More than 1 year has passed since last update.

reduxflowtypeを組み合わせたサンプルはreduxのサンプルにあるが、redux-thunkとの組み合わせの例はない。この記事では、reduxflowtypeのサンプルを示したあと、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のstoredispatchの定義使いまわすことで型定義の恩恵を得られる。
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