Ты смоделируешь свои состояния размеченными объединениями и закрепишь их полноту с помощью типа never.
Открыть этот урок в Kodokonnever - это пустой тип: ни одно значение ему не принадлежит. Он появляется, когда компилятор доказывает, что путь выполнения невозможен - например, после того как исчерпаны все члены объединения. Это свойство превращает его в инструмент проектирования: если ты добился, чтобы в ветке default у switch компилятор говорил never, у тебя есть статическое доказательство, что обработаны все случаи. Это основа проверки полноты.
С размеченными объединениями ты познакомился в прошлом модуле; теперь пора превратить их в полноценный инструмент проектирования. По сравнению с одним интерфейсом, набитым необязательными полями, размеченное объединение делает невозможные состояния непредставимыми: у тебя не может быть одновременно data и error или успеха без данных. Это основной паттерн для состояний загрузки, результатов операций и событийных сообщений.
type FetchState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
function render(state: FetchState<string[]>): string {
switch (state.status) {
case "idle": return "Waiting";
case "loading": return "Loading…";
case "success": return state.data.join(", ");
case "error": return state.error.message;
}
}Со стражем assertNever, вызываемым в ветке default, ты уже сталкивался; теперь разберём точно, как он работает. Пока покрыты все случаи, его параметр имеет там тип never, и всё компилируется. Добавь вариант в объединение, не обновив switch, - и забытый вариант перестанет быть совместимым с never: ошибка компиляции, а в сообщении - имя пропущенного случая. Ты превращаешь баг в продакшене в отказ сборки.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "rect"; width: number; height: number };
function assertNever(value: never): never {
throw new Error("Unhandled case: " + String(value));
}
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.side ** 2;
case "rect": return shape.width * shape.height;
default: return assertNever(shape);
}
}Две продвинутые тонкости заслуживают твоего внимания. Во-первых, сужение по дискриминанту распространяется на вложенные структуры, только если ты проверяешь сам охватывающий объект; деструктуризация одного лишь дискриминанта рвёт эту связь (кроме случаев совместной деструктуризации, которые поддерживаются с TypeScript 4.6). Во-вторых, never поглощается в объединениях (string | never - это просто string): именно это позволяет условным типам отсеивать варианты, как делает Exclude.
type Event =
| { type: "click"; x: number; y: number }
| { type: "keypress"; key: string };
type EventOf<T extends Event["type"]> =
Extract<Event, { type: T }>;
type ClickEvent = EventOf<"click">;
function handle<T extends Event["type"]>(
type: T,
handler: (event: EventOf<T>) => void
): void {
/* register the handler */
}
handle("click", (e) => console.log(e.x, e.y));Shape, не изменив switch с assertNever?string | never?neverstringunknownstring | undefined