Типизация состояний хука загрузки — idle, loading, success, error
useResource проводит запрос через четыре состояния: idle, loading, success (несёт data) и error (несёт error). Сейчас он отдаёт плоский объект с тремя необязательными полями, поэтому вызывающий может прочитать data, пока status ещё loading, а TypeScript промолчит.
Перетипизируйте ResourceState<T> так, чтобы data был доступен только в состоянии success, а error — только в состоянии error, и чтобы невозможную комбинацию нельзя было собрать вовсе. Никаких as, никакого any, никаких non-null assertion.
type ResourceState<T> = {
status: 'idle' | 'loading' | 'success' | 'error';
data?: T;
error?: Error;
};
// ваш код здесь — перетипизируйте ResourceState<T>
declare function useResource<T>(url: string): ResourceState<T>;
Допишите реализацию.
Опишите состояние одним размеченным объединением — {status:'idle'} | {status:'loading'} | {status:'success'; data:T} | {status:'error'; error:Error} — вместо плоского объекта с необязательными полями. Сужение по status откроет data только в ветке success, и loading с данными станет невыразимым.
- ✗Описывать четыре состояния одним объектом с необязательными полями, допуская невозможные комбинации
- ✗Хвататься за
data!в ветке success вместо того, чтобы дать сужению доказать наличие поля - ✗Считать, что обобщённый
Tне может жить только в одном члене размеченного объединения
- →Почему
status: 'loading'с заполненнымdata— это ошибка, которую плоский тип не поймает? - →Как добавить состояние
refetching, сохраняющее видимыми предыдущиеdata?
Решение
type ResourceState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
declare function useResource<T>(url: string): ResourceState<T>;
function View() {
const state = useResource<User>('/api/me');
switch (state.status) {
case 'idle': return null;
case 'loading': return <Spinner />;
case 'success': return <Profile user={state.data} />; // data есть, error нет
case 'error': return <Alert text={state.error.message} />;
}
}
Почему плоский объект неверен
{ status; data?; error? } описывает 4 статуса × 2 × 2 = 16 состояний, из которых осмысленны четыре. status: 'loading' с заполненным data — легальное значение этого типа, хотя такого состояния не существует. Отсюда и data! в коде: тип не может доказать то, что программист знает, и приходится затыкать его assertion.
Размеченное объединение оставляет ровно четыре обитаемых состояния. T спокойно живёт только в члене success — параметр типа принадлежит всему объединению, а не каждому его члену.
Дополнительный бонус — исчерпывающая проверка: если добавить пятый статус и забыть ветку, never-проверка в default не скомпилируется.
default: {
const unreachable: never = state; // ❌ если добавлен новый статус
return unreachable;
}