以前書いた記事で書いたReducerでは、受け取ったActionの型を判別するために、switch
で分岐した後に対応したActionの型でキャストをする必要があった。
const todoReducer: Reducer<ITodoState> = (
state: ITodoState = initTodoState,
action: TodoAction
): ITodoState => {
switch (action.type) {
case TodoActionType.ADD_TODO:
const addTodoAction: IAddTodoAction = action; // <-- ここでキャストしている
return {
...state,
todos: state.todos.concat([addTodoAction.payload.todo])
};
default:
return state;
}
};
この方法だと、action.type
とそれに対応する型を実装者が気にしなければならず、型安全とは言い切れなかった。
このあたりの話を某Slackで相談したら、以下のような Flux standard actionのtypeやpayloadをジェネリクスで指定するようにして、typeにenumを使うというやり方を教えてもらった。
import { Reducer } from 'redux';
// Flux standard action
interface Action<TType, TPayload = null, TMeta = undefined> {
type: TType;
payload: TPayload;
meta?: TMeta;
}
enum ActionType {
ADD,
DELETE
}
type AddAction = Action<ActionType.ADD, { todo: string }>;
type DeleteAction = Action<ActionType.DELETE, { todo: string }>;
type TodoAction = AddAction | DeleteAction;
interface TodoState {
todos: string[];
}
// Reducer
type TodoReducer = Reducer<TodoState, TodoAction>;
const initialState = {
todos: []
};
const reducer: TodoReducer = (
state: TodoState = initialState,
action: TodoAction
): TodoState => {
switch (action.type) {
case ActionType.ADD: {
return {
...state,
todos: [...state.todos, action.payload.todo]
};
}
case ActionType.DELETE: {
return {
...state,
todos: state.todos.filter(todo => todo !== action.payload.todo)
};
}
}
};
export default reducer;
こうするとSwitchでtypeを引っ掛けた時に型が推論されるため、caseのスコープの中でpayloadの型を確定できる。
(これを知った後にちゃんと調べたらReduxの公式ドキュメント(Usage with TypeScript)に型安全に各やり方が書いてあった……ちゃんとドキュメントは読みましょうということでした……)