any, unknown, never: верх, низ и лазейка
any отключает проверку и тихо расползается; unknown — безопасный верхний тип, который надо сузить до использования; never — пустой нижний тип, питающий проверки исчерпания. Понимание места каждого на решётке совместимости — базовая беглость в системе типов.
Прод-баг тянется к единственному JSON.parse(body), чей результат был типизирован как any. Оттуда any утёк в обработчик запроса, потом в вызов базы, потом в шаблон — и на каждом шаге TypeScript переставал проверять. Полгода правок компилировались зелёными поверх дыры в типах, которую никто не видел. Лекарством было не больше типов; одно ключевое слово: unknown на границе. Эти три особых типа — any, unknown, never — разница между системой типов, которая вас защищает, и той, что вы тихо выключили.
any — отказ от проверки, и смотрите, как он расползается
any — это не «какой-то тип», это «перестань проверять это значение». Что угодно совместимо с any, и any совместим с чем угодно. Опасна вторая половина: она позволяет значению тихо втечь в позиции, требующие конкретного типа.
const data: any = JSON.parse('{"port": "oops"}');
const port: number = data.port; // ✅ ошибки нет — `any` -> number разрешено
// ^? const port: number — но во время выполнения это строка "oops"
port.toFixed(2); // компилируется; падает в рантайме: toFixed is not a functionХуже того, any заразен. Прочтите свойство any, вызовите его, проиндексируйте — результат тоже any, поэтому дыра распространяется по всей цепочке вызовов:
function getUser(): any { /* ... */ }
const u = getUser(); // any
const name = u.profile; // any — всё ещё без проверки
const upper = name.bigify(); // any — `.bigify` не существует, но ошибки нетОдин any в источнике тихо отравляет всё ниже по течению. Поэтому any должен быть редким, локальным и намеренным — никогда не типом границы вроде распарсенного JSON-тела или нетипизированного результата библиотеки.
unknown — безопасный верхний тип
unknown — типобезопасный аналог any. Как и any, это верхний тип: всякое значение совместимо с ним. В отличие от any, со значением unknown нельзя сделать ничего, пока вы не сузите его до чего-то конкретного.
const data: unknown = JSON.parse('{"port": 8080}');
const port: number = data.port;
// Error: 'data' is of type 'unknown'.
// Property 'port' does not exist on type 'unknown'.
if (typeof data === "object" && data !== null && "port" in data) {
const p = data.port;
// ^? const p: unknown — всё ещё unknown, пока не сужено дальше
}unknown заставляет доказать форму до использования — через typeof, in, защитник типа или рантайм-валидатор (Zod, Valibot). Это правильный тип для всякой недоверенной границы: JSON.parse, fetch().json(), localStorage, полезные нагрузки шин сообщений. Детальная механика сужения — в следующем юните; здесь суть лишь в том, какой тип брать: unknown, не any.
▸Почему это работает
Цена any, в числах. Ущерб не теоретический — радиус поражения можно измерить. JSON.parse и Response.json() типизированы как any в стандартной библиотеке, поэтому кодовая база, которая их никогда не оборачивает, тихо наследует сотни any. На одном API-сервисе из ~1500 файлов мы прогнали type-coverage, и он показал 91.4% типизировано — недостающие 8.6% (~14 000 выражений) почти целиком тянулись к десятку необёрнутых границ, чей any расползся ниже по течению. Два из трёх прод-инцидентов того квартала (.toFixed на строке, undefined-id, дошедший до запроса) скомпилировались зелёными ровно потому, что any отключил проверку на границе. Лекарство было механическим: переключить эти границы на unknown + схему Zod, и правила @typescript-eslint/no-unsafe-* подсвечивают каждое недоказанное обращение. Компромисс, который сеньор называет явно: any даёт ноль трения сразу (обращайся к чему угодно немедленно), но счёт отложен и нарастает — рантайм-падение плюс каждое тихо отравленное место вызова ниже по течению; unknown берёт с вас одно сужение/валидацию на границе (несколько строк, распарсено один раз), а взамен вся цепочка ниже остаётся проверенной. Заплатите граничный налог один раз; не дайте any начислять проценты.
▸Граничные случаи
С TS 4.4 при useUnknownInCatchVariables (включено под strict) переменная e в catch (e) типизируется как unknown, а не any — ведь бросить можно что угодно, не только Error. Поэтому надо сузить до обращения: if (e instanceof Error) console.log(e.message). Одно это изменение закрыло огромный класс падений «Cannot read property ‘message’ of undefined» в блоках catch.
never — пустой нижний тип
Когда значение доказуемо не может существовать? Чаще, чем кажется — и когда вы используете этот факт намеренно, never становится самым честным сигналом, который может вернуть функция.
never — тип без значений, пустое множество. Это нижний тип (bottom type — тип, располагающийся в самом низу решётки совместимости): совместим с любым типом, но ничто (кроме never) не совместимо с ним. Он появляется всюду, где значения доказуемо не может быть:
function fail(msg: string): never { // никогда не возвращает нормально
throw new Error(msg);
}
function loop(): never { // никогда не завершается
while (true) {}
}
type Impossible = string & number;
// ^? type Impossible = never — нет значения, которое одновременно string И numberЦеннейшее применение — проверка исчерпания (exhaustiveness). Сузив объединение до ничего, оставшийся тип — never, и его можно утвердить, чтобы вызвать ошибку компиляции, если когда-нибудь добавят новый случай:
type Shape =
| { kind: "circle"; r: number }
| { kind: "square"; side: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.r ** 2;
case "square": return s.side ** 2;
default: {
const _exhaustive: never = s; // здесь s сужено до `never`
return _exhaustive;
}
}
}Добавьте { kind: "triangle"; base: number; height: number } в Shape и забудьте обработать — и ветка default теперь видит s как этот тип треугольника, а не never, поэтому const _exhaustive: never = s выдаёт ошибку:
// Error: Type '{ kind: "triangle"; ... }' is not assignable to type 'never'.Крошечный хелпер assertNever превращает это в однострочник, который вы кладёте в каждый default:
function assertNever(x: never): never {
throw new Error(`Unhandled case: ${JSON.stringify(x)}`);
}
// default: return assertNever(s);Это превращает «я забыл случай» из рантайм-сюрприза в ошибку времени компиляции — один из самых рычажных паттернов во всём языке.
▸Почему это работает
Почему never совместим со всем? Потому что это пустое множество: значения типа never никогда не может существовать, поэтому утверждение вроде «считай этот never числом» вакуумно безопасно — нет значения, которое могло бы это нарушить. Именно поэтому assertNever(s) проходит проверку, лишь когда s сужено до never; если какой-то случай не обработан, s — реальный тип с реальными значениями, и присваивание отклоняется.
Решётка совместимости
Представьте типы как решётку. unknown наверху (всё в него входит), never внизу (он входит во всё), конкретные типы посередине. any на самом деле в решётке не живёт вовсе — это боковая лазейка, одновременно и верх, и низ, что и делает его несостоятельным (unsound):
Вы парсите недоверенное JSON-тело запроса. Какой тип должен иметь распарсенный результат для максимальной безопасности?
Упорядочьте от ВЕРШИНЫ решётки совместимости к НИЗУ (от самой допускающей цели к самой узкой):
- 1 unknown — верхний тип; всякое значение с ним совместимо
- 2 string — конкретный тип посередине
- 3 "hello" — литеральный тип, подмножество string
- 4 never — нижний тип; совместим со всем, с ним совместимо ничто
- 01Почему один `any` так опасен? Проследите, как он распространяется и какую защиту убирает.
- 02Что такое `unknown`, чем он отличается от `any` и где его использовать?
- 03Что такое `never`, где он возникает и как включает проверку исчерпания?
any, unknown и never — углы системы типов: лазейка, безопасный верх и пустой низ. Нить, связывающая их — сужение (narrowing — сужение типа до более конкретного через проверку): unknown становится полезен, лишь когда вы его сужаете, а never — буквально то, что остаётся, когда сужение исчерпывает объединение. Это весь предмет урока о сужении в следующем юните (ваша цель deepensInto): typeof, in, instanceof, размеченные объединения и пользовательские защитники типов. А перед этим завершающий урок основ — литеральные типы — подхватывает нить расширения из вывода и показывает, как as const и объединения литералов дают точные, похожие на enum типы, не отказываясь от структурной типизации. Теперь, когда увидите any в сигнатуре граничного типа — распарсенный JSON, результат библиотеки, переменная catch — отнеситесь к нему как к незапертой машине на парковке: технически доступно, но проблема, которая ждёт своего часа.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.