Edited at

Typescriptでreduxのdispatchを行う際、インテリセンスを簡単に働かせるための型定義


概要

Typescript(以下TS)でstore.dispatch呼ぶ際、ジェネリクスを指定することで、type毎に必要なパラメータをインテリセンスを効かせることが出来ます。

ですが、Actionを定義する毎に色々なdispatchに足して回るのは手間ですので、1箇所変更するだけで良いようにサンプルの型定義を作りました。

(redux-thunk用にコールバックを渡した場合も含む)

※ reducerの数が巨大すぎるなど、reducer個別のdispatchを呼んでいる場合は話が変わるので対象外です


確認環境


  • vscode 1.31.0-insider

  • typescript 3.3.0-dev.20190109


書き方サンプル


dispatch.ts


import { store, AllAction } from './store';

/**
* アクションをそのままdispatchする場合
*/

export function dispatch<T extends AllAction>(action: T): T;
/**
* redux-thunk用にコールバックを渡す場合
* 通常のdispatchはコールバックを引数にとれない型定義なので、こちらで定義したdispatch関数を再帰的に引数型とすることで回避しています
*/

export function dispatch<T>(action: (aDispatch: typeof dispatch) => T): T;
export function dispatch(action: any) {
return store.dispatch(action);
}



store.ts


import { applyMiddleware, combineReducers, createStore } from 'redux';
import thunk from 'redux-thunk';
import { UserReducer as user } from './reducers/user_reducer';
import { CompanyReducer as company } from './reducers/company_reducer';

/**
* 2番目の引数(reducer毎のActionの引数)を取得する型定義
*/

type SecondArg = T extends (first: any, second: infer A, ...arg: any[]) => any ? A extends never ? never : A : never;

/**
* システム内で使用する全てのreducerを並べて下さい
*/

export const reducers = { user, company };
/**
* `typeof reducers`で上の変数の型を取得します
* 連想配列型なので、`keyof`を使って各値部分を取得することで、全ての値部分の型 = 全てのreducerを`|`で区切った型が取得できます
* あとは、2番目の引数型を抜き出してやれば、全てのアクションの型が取得できます
*/

export type AllAction = SecondArg<typeof reducers[keyof typeof reducers]>;
const rootReducer = combineReducers(reducers);
export const store = createStore(rootReducer, applyMiddleware(thunk));



user_reducer.ts


export {
reducer as UserReducer,
State as UserState,
Type as UserActionType,
};

function createInitialState(){
return {
id: 0,
name: '',
};
}

/**
* 第2引数は本来全てのaction型が来ますが、switchしてチェックするやり方なら必要なもののみに絞っても問題ないので、このreducerが扱うもののみを想定しています
*/

function reducer(state = createInitialState(), action: Action): State {
switch(action.type) {
case Type.SET:
return {
...state,
id: action.id,
name: action.name,
};
case Type.CLEAR:
return createInitialState();
}
}

enum Type {
SET = 'UserReducerSet',
CLEAR = 'UserReducerClear',
}

/**
* アクション毎に必要なtype以外のプロパティ
* ※そのまま書いたらプロパティ名が消えたので、エスケープ文字を仮に当てています
*/

interface Actions {
\[Type.SET\]: {
id: number;
name: string;
};
\[Type.CLEAR\]: {
};
}

/** Actionsに定義されたプロパティ群とtypeを合成し、アクションの形式に整形します */
type ActionTypeCreator<T> = { [P in keyof T]: { type: P; } & T[P]; }[keyof T];
type Action = ActionTypeCreator<Actions>;
type State = ReturnType<typeof createInitialState>;


型計算の詳細は過去に記事にしたこちらを参考にして下さい。

https://qiita.com/recordare/items/58745ef66dd9162e4559

新規にreducerを作る場合、store.tsreducersにreducerを足せばOKです。

また、user_reducer.tsTypeにアクション名を、Actionsに必要なパラメータを足すことで、新しいアクションを追加可能です。


user_action_creator.ts

import { dispatch } from './dispatch';

import { UserActionType } from './reducers/user_reducer';

export function setUserData(id:number, name:string){
return dispatch(cDispatch => {
return getUserInfoApi().then(res => {
cDispatch({
type: UserActionType.SET,
id: 'ng', // => numberじゃないので警告が出る,
name: res.name,
});
});
});
}


dispatchしてるからCreatorじゃないとかいうツッコミは勘弁下さい