any, unknown и never: в чём разница
Вопрос с собеседования: в чём разница между этими двумя функциями и почему вторая «безопаснее», хотя обе принимают вообще любое значение?
function processAny(data: any) {
console.log(data.toUpperCase()); // компилируется без единой жалобы
}
function processUnknown(data: unknown) {
console.log(data.toUpperCase()); // ОШИБКА КОМПИЛЯЦИИ
}Вывод (компилятор):
processUnknown: Object is of type 'unknown'.ts(18046)Первая функция спокойно проходит проверку типов, а на рантайме упадёт, если ей передать число: processAny(42) вызовет 42.toUpperCase(), а у чисел такого метода нет. Вторая функция вообще не даст себя скомпилировать, пока ты не докажешь компилятору, что data — строка. И вот это различие — ядро вопроса.
any — это буквально способ сказать компилятору TypeScript: «отключись, не проверяй это значение». Формально any тоже тип, но он ведёт себя как дыра в системе типов: любое значение можно присвоить переменной типа any, и саму переменную any можно присвоить куда угодно, и с ней можно делать любые операции — вызывать методы, обращаться к полям, складывать со строками — компилятор всё пропустит молча. По сути ты выключаешь ради этой переменной весь смысл TypeScript и возвращаешься в обычный JavaScript, только без предупреждения об этом в коде.
unknown появился в TypeScript 3.0 именно как безопасная альтернатива. Он тоже означает «значение любого типа», но с одним принципиальным ограничением: пока ты не сузил тип (не доказал компилятору, что перед ним конкретно), с значением типа unknown нельзя делать вообще ничего — ни вызывать методы, ни обращаться к полям, ни использовать в арифметике. Единственное, что доступно сразу, — это проверки типа: typeof, instanceof, сравнения с null и подобные. Только после такой проверки TypeScript «открывает доступ» к операциям, которые уместны для доказанного типа.
function processUnknownFixed(data: unknown) {
if (typeof data === "string") {
console.log(data.toUpperCase()); // ОК: тип сужен до string
} else {
console.log("Это не строка");
}
}
processUnknownFixed("привет");
processUnknownFixed(42);Вывод:
ПРИВЕТ
Это не строкаПрактический смысл: unknown заставляет тебя (или того, кто вызывает твою функцию) явно разобраться, что за значение пришло, прежде чем с ним что-то делать. Это особенно ценно на границах программы — там, где данные приходят снаружи и их тип заранее неизвестен: ответ от сервера после fetch, результат JSON.parse, данные из формы, аргументы в catch у ошибки. Именно поэтому в современном TypeScript тип пойманной ошибки в catch (e) по умолчанию — unknown, а не any: раньше можно было писать e.message и не думать, а теперь компилятор сначала попросит убедиться, что e — действительно объект ошибки.
Третий тип из вопроса — never — стоит особняком: это тип значения, которого не существует в принципе. Не «пустое значение», а «значение никогда не будет присвоено». Он появляется в двух ситуациях. Первая — функция никогда не завершается нормально: либо всегда бросает исключение, либо уходит в бесконечный цикл.
function fail(message: string): never {
throw new Error(message);
}
function getStatus(code: number): string {
if (code === 200) return "OK";
if (code === 404) return "Not Found";
return fail("неизвестный код"); // never совместим с любым возвращаемым типом
}Вторая, более важная для собеседования ситуация — never как результат исчерпывающей проверки (exhaustiveness check) в switch по union-типу. Если ты обработал все варианты union-а, то в ветке default остаётся тип never — и если позже кто-то добавит новый вариант в union, но забудет обработать его в switch, компилятор сам укажет на ошибку, потому что необработанное значение перестанет быть совместимо с never.
type Shape = "circle" | "square";
function describe(shape: Shape): string {
switch (shape) {
case "circle":
return "Круг";
case "square":
return "Квадрат";
default: {
const _exhaustive: never = shape; // если добавят "triangle" — ошибка тут
return _exhaustive;
}
}
}Как это работает под капотом. В теории типов any называют одновременно и top type, и bottom type — он ведёт себя непоследовательно именно потому, что отключает проверки в обе стороны. unknown — честный top type: в него можно поместить что угодно (как в any), но достать из него можно только после проверки (в отличие от any). never — bottom type, полная противоположность: он не содержит вообще ни одного значения, зато совместим с любым другим типом при возврате, потому что раз функция гарантированно не возвращает управление, обещание «я верну строку» никогда не будет нарушено — оно просто никогда не проверяется.
Частые ошибки на собеседовании: путают unknown с any, говоря, что они «одинаковые, просто unknown построже» — на деле это разное поведение, а не разная степень строгости одного и того же. Ещё частая ошибка — писать data as any при первой же ошибке компиляции вместо того, чтобы явно сузить тип: это лечит симптом (ошибку компилятора), но убирает всю пользу от типизации именно там, где она нужна. Наконец, путают never с void: void означает «функция ничего не возвращает (или возвращает undefined)», а функция при этом нормально завершается; never означает, что до конца функции выполнение вообще не доходит.