Сужение типов: проверки значений, type guards и поток управления
Автор: Казачкин Даниил Михайлович · Обновлено
Сужение типа связывает обычную проверку JavaScript с информацией, доступной компилятору в конкретной ветке. Правильная проверка одновременно объясняет поведение программы…
Сужение типа связывает обычную проверку JavaScript с информацией, доступной компилятору в конкретной ветке. Правильная проверка одновременно объясняет поведение программы человеку и разрешает операции, которые до проверки были небезопасны.
Цель и предпосылки
Нужны понятия unknown, объединения и сравнения JavaScript. Мы научимся читать тип по пути выполнения, отличать проверку существования поля от проверки его содержимого и избегать потери корректных нулевых значений. Примеры не требуют сервера: их можно выполнить в TypeScript Playground с strict и посмотреть одинаково воспроизводимые результаты.
Проверка должна отвечать на точный вопрос
В систему поступает оценка учебного материала. Разрешены числа от нуля до пяти. Значение ноль важно: это полноценная оценка, а не отсутствие данных. Если начать с if (!value), ноль будет отброшен вместе с null и undefined. Условие JavaScript проверяет истинность, тогда как бизнес-правилу нужно проверить числовой тип и диапазон. Похожая ошибка возникает с пустой строкой, когда она имеет собственное допустимое значение.
Основной пример принимает неизвестный объект и последовательно уточняет знания о нём. После исключения null разрешается проверка in. После неё известно наличие свойства, но не то, что внутри число. Только проверка самого поля позволяет сравнить его с границами и вернуть типизированный результат.
export {};
function readRating(value: unknown): number | undefined {
if (typeof value !== 'object' || value === null) return undefined;
if (!('rating' in value)) return undefined;
const rating = value.rating;
if (typeof rating !== 'number' || !Number.isFinite(rating)) return undefined;
if (rating < 0 || rating > 5) return undefined;
return rating;
}
console.log(readRating({ rating: 0 }));
console.log(readRating({ rating: 4.5 }));
console.log(readRating({ rating: '5' }));
console.log(readRating(null));Ожидаются четыре строки: 0, 4.5, undefined, undefined. Ранние возвраты исключают неподходящие входы. В последней строке функции компилятор знает, что rating — число: другие пути уже завершились. Сохранение поля в локальную константу также делает пример понятнее: дальнейшее рассуждение относится к одному прочитанному значению, а не к повторным обращениям к потенциально изменяемому объекту.
Наличие свойства и собственное свойство
Оператор in проверяет свойство с учётом цепочки прототипов. Для демонстрации анализа типов это удобно, но не всегда соответствует формату внешнего документа. Если правило требует исключительно собственное поле объекта, нужна соответствующая проверка во время выполнения. Не переносите пример механически на любой вход: сначала определите, что считать допустимым объектом, можно ли принимать массив, допустимы ли дополнительные свойства.
Для JSON после обычного разбора набор возможных объектов более ограничен, чем для произвольного JavaScript-значения. Для плагина, объекта с методами или прокси предположения могут отличаться. Выбор проверки зависит от границы приложения. Сужение описывает доказанную информацию, но не делает любую выбранную проверку универсальным валидатором всех возможных объектов.
Предикат удобно переиспользовать, но ему нужно доверять
Функция с результатом value is SomeType сообщает вызывающей стороне условие, при котором значение следует считать определённым типом. TypeScript не доказывает автоматически, что тело такой функции полностью соответствует заявленному условию. Предикат, который всегда возвращает true, способен солгать. Поэтому маленькая проверка должна быть понятной и покрывать как допустимые, так и близкие недопустимые значения.
export {};
function isText(value: unknown): value is string {
return typeof value === 'string';
}
const candidates: unknown[] = [' тема ', '', null, 9, 'разбор'];
const titles = candidates
.filter(isText)
.map((title) => title.trim())
.filter((title) => title.length > 0);
console.log(JSON.stringify(titles));Вывод — ["тема","разбор"]. Предикат isText проверяет ровно принадлежность к строкам, а очистка и проверка непустоты выполняются отдельно. Это существенно: обещание value is string работает в обе стороны. В истинной ветке TypeScript оставляет строки, в ложной — исключает их. Если такой предикат дополнительно отвергает пустую строку, ложная ветка перестаёт быть надёжной: там всё ещё может находиться строка. Поэтому содержательные ограничения нельзя незаметно добавлять в предикат обычного string.
Как читать сложное условие
Разбивайте условие на последовательность фактов: значение не null, оно имеет объектную форму, нужное поле присутствует, содержимое имеет подходящий примитивный тип, затем выполнено предметное ограничение. Порядок особенно важен при коротком замыкании &&: правая часть выполняется только после истинной левой. Если выражение стало трудно читать, несколько ранних возвратов часто лучше одной длинной строки с утверждениями типов.
Не все привычные проверки дают одинаковое сужение. typeof полезен для примитивов, instanceof — для отношений с конкретным конструктором, литеральное поле — для вариантов сообщения. Аннотация boolean у переменной сама по себе не хранит произвольное доказательство о любых других данных. При рефакторинге сложных проверок полезно смотреть тип непосредственно в месте операции, а не только рядом с исходным условием.
Типичные ошибки
Не обращайтесь к value.rating до проверки объекта; typeof null тоже возвращает 'object'. Не заменяйте распознавание типа строкой as: компилятор перестанет спрашивать о доказательстве, но код проверки не появится. Не используйте неполный type guard, проверяющий только одно поле большого интерфейса. И не смешивайте нормализацию с распознаванием незаметно для потребителя: превращение '5' в 5 меняет правила приёма данных.
Практикум: безопасное описание поиска
Функция получает unknown и должна вернуть строку вида Поиск: запрос, только если вход — объект с полем query, содержащим непустую строку после удаления крайних пробелов. Во всех остальных случаях верните Нет запроса. Проверьте объект с ' типы ', пустую строку, число вместо строки, отсутствующее поле и null. Сначала запишите ожидаемые ответы, затем реализуйте проверки.
Разбор: отделите объектную форму от содержимого поля, сохраните строку локально, выполните trim и проверьте длину. Для первого входа получится Поиск: типы; остальные случаи вернут запасной текст. Такой разбор показывает не только типовую безопасность, но и выбранную нормализацию. Дополнительное упражнение — разрешить пустой запрос как команду «показать всё». Тогда нужно изменить условие содержимого и ожидаемый результат, а не ослаблять тип входа.
Частые вопросы
Зачем проверять null после typeof value === 'object'?
Таково поведение JavaScript: null проходит проверку на строку 'object', хотя обращаться к его свойствам нельзя. Отдельное исключение null нужно и для безопасного исполнения, и для корректного понимания последующих операций компилятором.
Может ли type guard заменить проверку схемы?
Для маленького и стабильного набора полей ручной предикат может быть достаточным. Для вложенных данных, нескольких вариантов и подробных сообщений об ошибках стоимость самостоятельной проверки растёт. Выбирайте инструмент по сложности контракта и сохраняйте тесты на неверные входы в обоих случаях.
Связанные исследования
- Типы как множества значений: полезная модель и её границы в TypeScript — Проверяем модель типов как множеств значений: union, intersection, unknown и never; контрпримеры с any, распределением условных типов и мутацией общих ссылок.