Сужение типов и объединения
Что это такое
Объединённый тип говорит, что значение — одно из нескольких возможных: string | number или { status: "loading" } | { status: "ready"; data: Item[] }. Сужение — приём, которым компилятор узнаёт, какой из вариантов перед ним: проверка дискриминанта в if, typeof, switch или собственная функция-страж типа. Вместе они позволяют писать состояния, в которых невозможно ошибиться, а не состояния, которые лишь задокументированы.
Почему это важно
Большая часть защитного кода существует потому, что два факта, обязанные совпадать, хранятся отдельно: логический флаг loading, поле data и строка error, которые могут противоречить друг другу. Объединение делает противоречие непредставимым — пока идёт загрузка, поля с данными нет, и прочитать его по ошибке не получится. В этом разница между проверкой типов, которая документирует ваши дефекты, и той, которая их предотвращает; обычно такая замена убирает больше кода, чем добавляет.
Ключевые идеи
Различайте варианты по литеральному свойству
Дайте каждому варианту литеральное поле с разным значением, и компилятор сможет разделить объединение обычным сравнением. Этот приём покрывает жизненный цикл запроса, отправку формы и любое сообщение, которое приложение передаёт между своими частями.
type LoadState =
| { status: "loading" }
| { status: "ready"; items: Item[] }
| { status: "error"; message: string };
function render(state: LoadState) {
switch (state.status) {
case "loading": return "Loading…";
case "ready": return listItems(state.items);
case "error": return showError(state.message);
}
}
Внутри каждой ветки остальные варианты недостижимы, поэтому state.items доступно только там, где оно существует.
Сужайте проверками, которые уже даёт язык
typeof, instanceof, in, Array.isArray и проверка на истинность — все они сужают. Проверки typeof x === "object" самой по себе недостаточно, ведь null — тоже объект; сравнивайте с ним отдельно.
function length(value: string | string[] | null): number {
if (value === null) return 0;
return typeof value === "string" ? value.length : value.length;
}
Сделайте исчерпываемость проверкой времени компиляции
Завершите switch по объединению ветвью по умолчанию: она присваивает значение переменной типа never. Если позже появится новый вариант, это присваивание перестанет компилироваться, и компилятор укажет каждое место, требующее правки, вместо того чтобы одно из них молча пропустило случай.
default: {
const unhandled: never = state;
throw new Error(`unhandled state: ${JSON.stringify(unhandled)}`);
}
Пишите страж типа, когда проверка повторяется
Функция, которая возвращает value is T, учит компилятор проверке, которую он не может вывести, и держит знание в одном месте. Проверяйте, а не предполагайте: страж, совравший компилятору, переносит сбой со времени компиляции на падение во время выполнения.
Предпочитайте маленькое объединение широкому объекту
Тип с десятком необязательных полей ничего не говорит о том, какие сочетания допустимы. Несколько небольших вариантов утверждают это точно и заставляют каждого потребителя обрабатывать каждый случай.
Практика
Замените три флага одним объединением
Найдите компонент или функцию, которая отслеживает loading, error и data отдельными переменными. Замените их размеченным объединением и switch, который заканчивается проверкой на never.
Готово, когда: код больше не спрашивает, есть ли данные, прежде чем их прочитать, и добавление четвёртого варианта ломает компиляцию, пока вы его не обработаете.
Напишите один переиспользуемый страж типа
Возьмите проверку времени выполнения, которую вы повторяете, — ответ API, который может быть полезной нагрузкой ошибки, значение конфигурации, которое может быть строкой, — и превратите её в функцию-страж.
Готово, когда: страж используется минимум в двух местах, а намеренная передача неверной формы в защищённое значение отклоняется компилятором.
Частые ошибки
- Проверка необязательного поля через
if (x.data)— законное ложное значение вроде0или""принимается за отсутствие; исправить: сужайте по дискриминанту, а не по полезной нагрузке. - Забытый
nullпри сужении черезtypeof—typeof null === "object", и проверка проходит для null; исправить: сравните сnullдо проверки типа. - Приведение через
asвместо сужения — компилятор замолкает, а значение во время выполнения не меняется; исправить: используйте дискриминант или проверенный страж типа. switchбез проверки исчерпываемости — новый вариант молча игнорируется; исправить: присвойте значение переменной типаneverв ветви по умолчанию.- Слишком охотный страж, возвращающий
true— тип утверждён, а форма неверна, и падение уезжает дальше по потоку; исправить: проверяйте каждое свойство, которое обещает страж.
Что читать дальше
- Система типов (02-frontend/19-typescript/01-the-type-system) — структурная типизация и вывод типов — механика, на которую опирается сужение.
- Обобщения (02-frontend/19-typescript/03-generics) — следующий шаг: сохранение связи между типами входа и выхода функции.
- Паттерны проектирования (01-cs-fundamentals/18-design-patterns) — размеченные объединения — типизированная форма нескольких классических паттернов.