TypeScriptでReduxのActionをどのように書くかは記事によってばらつきがあります。これからRedux+TypeScriptを書く人が迷わないようまとめたいと思います。
TL;DR
String enumsを使おう。
TypeScript2.4からEnums にString enumsが導入されています。2.4+以降の環境であればString enumsを使うことでより型安全にReduxが書けるよって記事です。
Enumsを使わないパターン
少し古い記事だと次のようにActionのtypeが文字列定数で定義されているパターンをみます。
const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
JSの場合に近い書き方ですが、それぞれのActionを定義する際にせっかくのTypeScriptの型を生かしづらいです。型を生かしてActionを書くとすると次のようになりちょっと冗長ですね。
const INCREMENT_COUNTER: 'INCREMENT_COUNTER' = 'INCREMENT_COUNTER';
interface IncrementCounterAction extends Action {
type: typeof INCREMENT_COUNTER;
payload: {
num: number;
};
}
export const incrementCounter = (num: number): IncrementCounterAction => ({
type: INCREMENT_COUNTER,
payload: {
num
}
});
他にtypeは宣言せずに直接文字列リテラルを指定しているパターンもあります。
interface IncrementCounterAction extends Action {
type: 'INCREMENT_COUNTER';
payload: {
num: number;
};
}
Reducerでtypeをチェックする際に、ありえないcaseを書くとコンパイルエラーになる(TypeScript 2.1+であれば)ので文字列リテラルも悪くはないですが、文字列をコピペするのはあまりしたくないですし、名前空間を用意してスコープを絞りたくなります。
TypeScript 2.4以降での実装
Actionのtypeを定義する
String enumsを用いて次のように書きましょう。
enum ActionTypes {
INCREMENT_COUNTER = 'INCREMENT_COUNTER',
DECREMENT_COUNTER = 'DECREMENT_COUNTER'
}
Action/ActionCreatorを定義する
次にActionとActionCreatorを定義します。
import { Action } from 'redux';
interface IncrementCounterAction extends Action {
type: ActionTypes.INCREMENT_COUNTER;
payload: {
num: number;
};
}
export const incrementCounter = (num: number): IncrementCounterAction => ({
type: ActionTypes.INCREMENT_COUNTER,
payload: {
num
}
});
interface DecrementCounterAction extends Action {
type: ActionTypes.DECREMENT_COUNTER;
payload: {
num: number;
};
}
export const decrementCounter = (num: number): DecrementCounterAction => ({
type: ActionTypes.DECREMENT_COUNTER,
payload: {
num
}
});
export type CounterActions = IncrementCounterAction | DecrementCounterAction;
typeには上で定義したEnumsのメンバーを指定します。また、今回の本題ではないですが、ActionはFlux Standard Actionに従って定義することをお奨めします。
// 微妙な書き方
interface IncrementCounterAction extends Action {
type: ActionTypes.INCREMENT_COUNTER;
num: number;
}
Reducerを定義する
合わせてReducerも書くと次のようになります。
export interface CounterState {
count: number;
}
const initialState: CounterState = {
count: 0
};
const reducer = (state: CounterState = initialState, action: CounterActions): CounterState => {
switch (action.type) {
case ActionTypes.INCREMENT_COUNTER:
return { count: state.count + action.payload.num };
case ActionTypes.DECREMENT_COUNTER:
return { count: state.count - action.payload.num };
default:
return state;
}
};
export default reducer;
ありえないcase値を指定しようとするとコンパイラに怒られますし、Tagged union typesもちゃんと効くので各case内で存在しないpayloadにアクセスする心配もありません。ガシガシ補完きかせられるので書くのもちょっと楽になります。
まとめ
String enumsを使って型の恩恵をより得ながらReduxを書こう!