Типизируйте reducer так, чтобы необработанный action ломал сборку
Reducer хранилища разбирает union Action через switch. Сейчас новый тип action молча проваливается в default, возвращает старое состояние, и баг всплывает только в интерфейсе.
Ограничения: отказ обязан быть ошибкой компиляции в каждом reducer, пропустившем новый action; проверка должна переиспользоваться во многих reducer, а не копироваться; ветка default обязана по-прежнему бросать во время выполнения, если плохой action всё же дойдёт.
type Action =
| { type: 'inc'; by: number }
| { type: 'dec'; by: number }
| { type: 'reset' };
interface State { count: number }
function reducer(state: State, action: Action): State {
switch (action.type) {
// ваш код здесь
}
}
Допишите реализацию.
Делайте switch по литеральному полю type и завершайте его default: return assertNever(action), где assertNever(value: never): never бросает исключение. Каждый разобранный case отсекает свой вариант: при полном union остаток равен never, а новый action ломает сборку.
- ✗Типизировать параметр
assertNeverкакunknownилиany, что принимает любой action - ✗Возвращать старое состояние из
default, что компилируется вечно и без жалоб - ✗Расширять union полем
typeтипаstring, из-за чего ничто не сужается доnever
- →Почему параметр
assertNeverобязан иметь типnever, а неunknown? - →Как это сочетается с union из action, собранным из многих слайсов?
Решение
Ключ — помощник, параметр которого имеет тип never. В него можно передать только то, чего не существует, поэтому вызов компилируется лишь при полностью разобранном union.
type Action =
| { type: 'inc'; by: number }
| { type: 'dec'; by: number }
| { type: 'reset' };
interface State { count: number }
function assertNever(value: never): never { // переиспользуется всеми reducer
throw new Error(`Unhandled action: ${JSON.stringify(value)}`);
}
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'inc': return { count: state.count + action.by };
case 'dec': return { count: state.count - action.by };
case 'reset': return { count: 0 };
default: return assertNever(action); // ✅ здесь action сужен до never
}
}
Добавьте { type: 'setTo'; value: number } — и каждый reducer, не разобравший его, падает:
// error TS2345: Argument of type '{ type: "setTo"; value: number; }'
// is not assignable to parameter of type 'never'.
⚠️ Параметр обязан быть never. С unknown или any подойдёт любой action, вызов будет компилироваться всегда, и вся проверка выродится в бросок во время выполнения. Ветка default при этом остаётся страховкой: если чужой JavaScript пришлёт неизвестный action, она бросит.