LoginSignup
138
100

More than 5 years have passed since last update.

reduxをtypescriptで使うならこれを使うしかない。(typescript-fsaがすごい)

Last updated at Posted at 2017-07-14

ReduxとTypeScriptっていまいち相性が良くない?

TypeScriptとReact.jsって相性が良いですよね。
実際Reactを使うのにTypeScriptとかflowを推奨している気がします
Reactの学習を始めるのにも補助輪として有効ではないかと思います。
その辺は 古いですが
TypeScriptを使ってreactのチュートリアルを進めると捗るかなと思った。
を見ていただければご理解いただけると思います。

さて、しかしながらです。この調子でReduxもTypeScriptで学習初めたら捗るかと思いましたが 上手く行きませんでした。

理由の一つとして

reduxにおいてAction は概念として重要なものですが その実態は type プロパティを持つobjectでしかない。 という点があります。

これが辛い。

TypeScriptにおいてどの様にActionを定義するべきか指針となる情報がないのです。
とりあえず、私は以下のようにActionを定義することにしました

actions.ts
export type Action =
    {
        type: 'INIT';
        payload: undefined;
    } |
    {
        type: 'FETCH_MAIN_FEEDS';
        payload: undefined;
    } |
    {
        type: 'SET_MAIN_FFEDS';
        payload: comm.Contentlist;
    } |
    {
        type: 'FETCH_ACCOUNT';
        payload: FetchAccountPayload;
    } |
    {
        type: 'SET_ACCOUNTS';
        payload: comm.Account[];
    } |
    {
        type: 'FETCH_LOGIN_TOKEN';
        payload: undefined;
    } |
    {
        type: 'SET_LOGIN_TOKEN';
        payload: string;
    } |
// 省略

この書き方はF8Appを参考にしています。とりあえずこの書き方はreducerを書く時に有効でした。

index_ts_—_taiya-ki-client.png

上記のようにtypescriptのTypeGuard機能によってaction.typeが SET_MAIN_FFEDSのルートで
action.payloadがContentlistだと推論してくれるわけです。

良さそうじゃないですか。

でもこの書き方だとactionCreatorを作るのがしんどくなります。
jsにおいてはactionを毎回書くのがしんどいのでactionCreatorという actionを返す関数 を作るのですが
TypeScriptでは、間違いを指摘してくれるのでactionを直接dispatchに書きたくなります(※個人の感想です)。
と言うか上記の書き方だと変更があった場合にactionCreatorとActionの両方の修正が必要となり冗長になります。
かと言ってactionCreatorのみの定義だと、上記のようなTypeGuardも使えなくなります。

辛い。

そんな中見つけたのが、 typescript-fsa です。

typescript-fsa の使いかた

とりあえずです。

あなたのreduxを使ったプロジェクトにプロジェクトに追加しましょう

$ yarn add typescript-fsa
$ yarn add typescript-fsa-reducers

多分 typescript-fsa-reducersも使うことになります。reducerを使う上での便利ライブラリです。

actionの変更

  • before
before
export type Action =
    {
        type: 'INIT';
        payload: undefined;
    } |
    {
        type: 'FETCH_MAIN_FEEDS';
        payload: undefined;
    } |
    {
        type: 'SET_MAIN_FFEDS';
        payload: comm.Contentlist;
    } |
    {
        type: 'FETCH_ACCOUNT';
        payload: FetchAccountPayload;
    } |
    {
        type: 'SET_ACCOUNTS';
        payload: comm.Account[];
    };
// 一部のみ

  • after
after
import actionCreatorFactory from 'typescript-fsa';

const actionCreator = actionCreatorFactory();

export const init = actionCreator('INIT');
export const fetchMainFeeds = actionCreator('FETCH_MAIN_FEEDS');
export const setMainFeeds = actionCreator<comm.Contentlist>('SET_MAIN_FFEDS');
export const fetchAccount = actionCreator<FetchAccountPayload​​>('FETCH_ACCOUNT');
export const setAccounts = actionCreator<comm.Account[]>('SET_ACCOUNTS');

// 一部のみ

こんな感じでactionCreatorを簡単に作れます。
これでちゃんと補完機能を使えるのでしょうか?
ポイントはactionCreator<ペイロードの型> という形式です。
actionCreatorなので当然dispatchするときはこんな書き方になります

props.dispatch(actions.init());

reducerの変更

さて前の書き方と変わってちゃんとtypeGuardが使えるのでしょうか?
結論:使えました。

  • before
before
export function loginToken(state: LoginToken = new LoginToken(), action: Action) {
    switch (action.type) {
        case 'SET_LOGIN_TOKEN':
            state = state.setLoginToken(action.payload);
            return state;
        case 'SET_LOGIN_INFO':
            state = state.setMyAccount(action.payload);
            return state;
        case 'LOGOUT':
            state = state.logout();
            return state;
        default:
            return state;
    }
}

  • after
after
import { reducerWithInitialState } from 'typescript-fsa-reducers';

export const loginToken = reducerWithInitialState(new LoginToken())
    .case(actions.setLoginToken, (state, payload) => (state.setLoginToken(payload)))
    .case(actions.setLoginInfo​​ , (state, payload) => (state.setMyAccount(payload)))
    .case(actions.logout, (state) => (state.logout()));

上記2つは全く同じ挙動でした。
ちゃんとペイロードの型が推論できている様子も貼っておきます。

index_ts_—_taiya-ki-client2.png

仕組み

作成したactionCreaterの型を見てみると次のようになっています。

index_ts_—_taiya-ki-client3.png

actionCreater内にペイロードの型情報を持っています。

case<P>(actionCreator: ActionCreator<P>, handler: Handler<InS, OutS, P>): ReducerBuilder<InS, OutS>;

caseの引数に渡されるactionCreator からPが推論できるのでhandlerの引数にペイロードの型情報(P)が渡されるわけです。

わかってしまえばなんてことはないですね。

参考

138
100
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
138
100