ReduxとTypeScriptっていまいち相性が良くない?
TypeScriptとReact.jsって相性が良いですよね。
実際Reactを使うのにTypeScriptとかflowを推奨している気がします。
Reactの学習を始めるのにも補助輪として有効ではないかと思います。
その辺は 古いですが
「TypeScriptを使ってreactのチュートリアルを進めると捗るかなと思った。」
を見ていただければご理解いただけると思います。
さて、しかしながらです。この調子でReduxもTypeScriptで学習初めたら捗るかと思いましたが 上手く行きませんでした。
理由の一つとして
reduxにおいてAction
は概念として重要なものですが その実態は type
プロパティを持つobjectでしかない。 という点があります。
これが辛い。
TypeScriptにおいてどの様にActionを定義するべきか指針となる情報がないのです。
とりあえず、私は以下のようにActionを定義することにしました
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を書く時に有効でした。
上記のように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
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
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
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
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つは全く同じ挙動でした。
ちゃんとペイロードの型が推論できている様子も貼っておきます。
仕組み
作成したactionCreaterの型を見てみると次のようになっています。
actionCreater内にペイロードの型情報を持っています。
case<P>(actionCreator: ActionCreator<P>, handler: Handler<InS, OutS, P>): ReducerBuilder<InS, OutS>;
caseの引数に渡されるactionCreator
からPが推論できるのでhandlerの引数にペイロードの型情報(P)が渡されるわけです。
わかってしまえばなんてことはないですね。
参考