Any, unknown и never: неизвестные значения и невозможные состояния
Автор: Казачкин Даниил Михайлович · Обновлено
any, unknown и never полезно различать по тому, какие действия они разрешают. Первый отключает существенную часть проверки, второй требует доказать возможность операции, а третий…
any, unknown и never полезно различать по тому, какие действия они разрешают. Первый отключает существенную часть проверки, второй требует доказать возможность операции, а третий обозначает отсутствие допустимого значения в рассматриваемой точке программы.
Цель и исходные знания
Вы уже знаете аннотации параметров и простые объектные типы. После урока сможете выбрать тип для непроверенного входа, не распространять any по приложению и объяснить роль never без метафоры «странное пустое значение». Примеры выполняются независимо друг от друга в TypeScript 6 с strict; намеренно неправильные строки находятся в невыполняемой ветке.
Один вход, две степени доверия
Представьте импорт длительности занятия из неизвестной системы. Иногда приходят минуты числом, иногда — строка с комментарием. Функция расчёта не должна делать вид, что оба значения одинаково пригодны для умножения. Нужна граница: до неё данные ещё неизвестны, после неё получено конкретное число с проверенными ограничениями. Тип unknown помогает удержать эту границу видимой для компилятора.
export {};
function requireMinutes(value: unknown): number {
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
throw new Error('Ожидалось конечное неотрицательное число минут');
}
return value;
}
const unchecked: any = { minutes: 'скоро' };
console.log(unchecked.minutes * 2);
console.log(requireMinutes(45) * 2);
try {
requireMinutes('45');
} catch (error: unknown) {
console.log(error instanceof Error ? error.message : 'Неизвестная ошибка');
}
if (false) {
const pending: unknown = '45';
// @ts-expect-error Нельзя вычислять с неизвестным типом без проверки.
console.log(pending * 2);
}Будут напечатаны NaN, 90 и сообщение о неверном числе минут. Первая операция прошла проверку типов только потому, что any разрешил её без достаточной информации. Вторая использовала результат проверяющей функции. Третья намеренно не преобразует строку в число: допустимость такого преобразования должна определяться форматом импорта, а не случайным поведением оператора умножения.
Проверка typeof value === 'number' сама по себе недостаточна для выбранного правила: NaN и бесконечность также относятся к number. Дополнительные условия описывают конкретную задачу. Если для другого продукта отрицательные числа имеют смысл, менять следует правило, а не выбирать более хитрый TypeScript-тип в надежде автоматически исправить данные.
Как any распространяет неопределённость
Из значения any легко получить другое any: прочитать свойство, вызвать метод, использовать возвращённый результат. После нескольких таких переходов неизвестно уже не только содержимое входа, но и корректность значительной части вычисления. Особенно неприятно, когда небезопасное значение выходит из маленькой вспомогательной функции с неявно выведенным результатом. Тогда потребители теряют проверки, хотя сами нигде не написали any.
Это не означает, что any запрещён как часть языка. Он встречается при постепенном переводе старого кода, в низкоуровневых адаптерах и некоторых типовых конструкциях библиотек. Практический вопрос — насколько мала область доверия и чем она заканчивается. На границе системы часто лучше сразу присвоить результат переменной unknown, проверить его и вернуть понятную модель. Нельзя считать unknown валидатором: он заставляет разработчика написать нужную проверку, но не выполняет её самостоятельно.
Где появляется never
Функция с результатом never не возвращает управление обычным способом: например, она всегда выбрасывает исключение. Это отличается от void, который говорит вызывающей стороне не рассчитывать на полезное возвращаемое значение. Другая важная ситуация — ветка после обработки всех вариантов объединения. Если ни одного допустимого случая не осталось, компилятор может представить оставшееся значение как never.
export {};
type Delivery = { kind: 'email'; address: string } | { kind: 'pickup'; desk: number };
function impossible(value: never): never {
throw new Error('Необработанный вариант: ' + JSON.stringify(value));
}
function destination(delivery: Delivery): string {
switch (delivery.kind) {
case 'email': return delivery.address;
case 'pickup': return 'Стойка ' + delivery.desk;
default: return impossible(delivery);
}
}
console.log(destination({ kind: 'pickup', desk: 4 }));Вывод — Стойка 4. Добавьте в Delivery третий вариант и не меняйте функцию: аргумент в impossible(delivery) перестанет соответствовать never. Ошибка показывает конкретное незавершённое изменение. Здесь не нужно создавать значение типа never; смысл проверки в том, чтобы доказать отсутствие оставшихся разрешённых вариантов.
Ошибки, которые скрываются за красивыми словами
Не присваивайте проверенному типу неизвестный вход через двойное утверждение as unknown as ...: такая цепочка не является двумя проверками. Не возвращайте never из функции, которая иногда доходит до конца. Не путайте «тип не содержит нормальных значений» с «в JavaScript есть специальное значение never». Типы существуют для анализа программы, а выполняемый код по-прежнему работает с обычными значениями и исключениями.
Практикум: импорт ограниченного количества мест
Напишите функцию requireCapacity(value: unknown): number. Допустимы только целые числа от одного до тридцати включительно. Проверьте 12, 0, 31, 2.5, '12', null и NaN. Затем оберните вызов в обработчик ошибки, который формирует сообщение для пользователя, не обращаясь к error.message до проверки формы ошибки.
Разбор: сначала нужен typeof, затем Number.isInteger, затем границы диапазона. Порядок делает каждую следующую операцию обоснованной. Строку '12' не принимают, поскольку условие задания не разрешает преобразование; если оно понадобится, это будет отдельная стадия импорта с собственными примерами. Обработчик использует error instanceof Error, а для другого брошенного значения даёт запасное сообщение. Полезный результат практики — таблица входов и ожидаемых исходов, а не только отсутствие красного подчёркивания в редакторе.
Частые вопросы
Чем [never] отличается от never?
[never] — тип кортежа длины один, чей обязательный элемент имеет тип never; это не сам never и не пустой кортеж []. Обычное допустимое значение элемента создать нельзя, но форма кортежа остаётся значимой для типовых операций. В условных типах обёртка квадратными скобками также используется для управления распределением по union.
Почему нельзя просто заменить любой any на unknown?
После механической замены часто обнаруживаются реальные неописанные предположения: какие свойства обязательны, какие значения допустимы, откуда они пришли. Их нужно разрешить проверками или корректными сигнатурами. Замена полезна как начало анализа, но исправление задачи состоит в восстановлении границ доверия, а не в выборе другого слова.
Связанные исследования
- Типы как множества значений: полезная модель и её границы в TypeScript — Проверяем модель типов как множеств значений: union, intersection, unknown и never; контрпримеры с any, распределением условных типов и мутацией общих ссылок.
- Что означает «TypeScript обнаруживает 15% ошибок»: критическое чтение исследования ICSE 2017 — Критический разбор ICSE 2017: 400 исторических ошибок, Flow 0.30 и TypeScript 2.0, обнаружимость 15%, ограничения метода и собственный протокол оценки пользы типов.