Когда говорят “TypeScript понимает action внутри switch”, это часто называют type guard. Технически точнее говорить о discriminated union: у каждого action есть общее поле type, и именно по нему TypeScript сужает тип внутри конкретной ветки.
Это особенно хорошо видно в reducer. Начнем с минимальных типов:
export interface Todo {
id: number;
title: string;
completed: boolean;
}
export type FetchTodosAction = {
type: "todos/fetched";
payload: Todo[];
};
export type DeleteTodoAction = {
type: "todos/deleted";
payload: number;
};
export type Action = FetchTodosAction | DeleteTodoAction;Теперь сам reducer:
export function todosReducer(state: Todo[] = [], action: Action): Todo[] {
switch (action.type) {
case "todos/fetched":
return action.payload;
case "todos/deleted":
return state.filter((todo) => todo.id !== action.payload);
default:
return state;
}
}Здесь и происходит главное. В ветке case "todos/fetched" TypeScript знает, что action имеет тип FetchTodosAction. В ветке case "todos/deleted" тот же action уже сужается до DeleteTodoAction.
Это значит, что payload тоже получает правильный тип без ручных проверок:
export function todosReducer(state: Todo[] = [], action: Action): Todo[] {
switch (action.type) {
case "todos/fetched":
action.payload; // Todo[]
return action.payload;
case "todos/deleted":
action.payload; // number
return state.filter((todo) => todo.id !== action.payload);
default:
return state;
}
}Почему это работает
TypeScript умеет сужать объединение, если у каждой ветки есть общий дискриминатор. В Redux этим дискриминатором почти всегда выступает строковое поле type.
Поэтому важна не сама библиотека Redux, а форма action:
- у каждого action должен быть стабильный
type; - у каждого
typeдолжен быть свой корректныйpayload; - reducer должен ветвиться по
action.type, а не пытаться угадывать форму объекта косвенно.
Если это соблюдено, TypeScript делает остальную работу сам.
Где здесь настоящий type guard
Пользовательский type guard нужен, когда одного switch недостаточно и ты хочешь вынести проверку в отдельную функцию:
function isDeleteTodoAction(action: Action): action is DeleteTodoAction {
return action.type === "todos/deleted";
}
export function todosReducer(state: Todo[] = [], action: Action): Todo[] {
if (isDeleteTodoAction(action)) {
return state.filter((todo) => todo.id !== action.payload);
}
if (action.type === "todos/fetched") {
return action.payload;
}
return state;
}Но в обычном reducer switch (action.type) обычно проще и читабельнее.