Сужение типов и объединения — Примеры
Пример 1 — Три флага, которые могут противоречить друг другу
Логический флаг и два необязательных поля отслеживают состояние загрузки, а отрисовка читает items, прежде чем проверить, есть ли они. Скажите, какие сочетания допускает такой тип, затем сделайте невозможные сочетания непредставимыми.
type LoadState = { loading: boolean; items?: Item[]; error?: string };
function render(state: LoadState) {
if (state.loading) return "Loading…";
if (state.error) return showError(state.error);
return listItems(state.items); // Item[] | undefined
}
Решение
type LoadState =
| { status: "loading" }
| { status: "ready"; items: Item[] }
| { status: "error"; message: string };
function render(state: LoadState): string {
switch (state.status) {
case "loading": return "Loading…";
case "ready": return listItems(state.items);
case "error": return showError(state.message);
default: {
const unhandled: never = state;
throw new Error(`unhandled state: ${JSON.stringify(unhandled)}`);
}
}
}
// после добавления четвёртого варианта: | { status: "empty" }
const unhandled: never = state;
~~~~~~~~~
error TS2322: Type '{ status: "empty"; }' is not assignable to type 'never'.
Три отдельных факта могут противоречить друг другу: загрузка при наличии данных, ошибка без сообщения. Размеченное объединение хранит один факт, поэтому невозможные сочетания нельзя написать, а внутри ветки компилятор знает, какие поля существуют, — state.items доступно только там, где оно реально. Присваивание переменной типа never и делает исчерпываемость проверкой времени компиляции, а не соглашением: добавьте вариант, и каждый switch, обязанный его обработать, перестанет собираться, так что ничто не будет молча пропущено.
Пример 2 — null, проходящий проверку на объект
Помощник принимает текст, список текстов или ничего и сужает значение через typeof, но случай null проскакивает в ту же ветку, что и список. Объясните, какой проверки не хватает, затем исправьте всё и вынесите проверку времени выполнения в отдельную функцию.
function length(value: string | string[] | null): number {
if (typeof value === "object") return value.length; // null — тоже объект
return value.length;
}
Решение
function length(value: string | string[] | null): number {
if (value === null) return 0;
return value.length;
}
type ApiResponse = { data: Item[] } | { error: string };
function isError(response: ApiResponse): response is { error: string } {
return "error" in response;
}
typeof null равно "object", поэтому для значения null выполняется ветка объекта, и сужение ошибается насчёт того, что оно доказало; проверка обязана сравнивать с null явно, прежде чем проверять тип. Повторяемая проверка времени выполнения должна жить в одной функции, возвращающей предикат типа, — тогда знание существует в одном месте, и каждый вызывающий код получает сужение, — а единственная ошибка, которой здесь нужно избегать, — страж, слишком охотно возвращающий true.