open atlas
↑ К треку
Система типов TypeScript вглубь TS · 01 · 03

any, unknown, never: верх, низ и лазейка

any отключает проверку и тихо расползается; unknown — безопасный верхний тип, который надо сузить до использования; never — пустой нижний тип, питающий проверки исчерпания. Понимание места каждого на решётке совместимости — базовая беглость в системе типов.

TS Junior ◷ 13 min
Уровень
ОсновыJuniorMiddleSenior

Прод-баг тянется к единственному 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. 1 unknown — верхний тип; всякое значение с ним совместимо
  2. 2 string — конкретный тип посередине
  3. 3 "hello" — литеральный тип, подмножество string
  4. 4 never — нижний тип; совместим со всем, с ним совместимо ничто
Вспомните перед уходом
  1. 01
    Почему один `any` так опасен? Проследите, как он распространяется и какую защиту убирает.
  2. 02
    Что такое `unknown`, чем он отличается от `any` и где его использовать?
  3. 03
    Что такое `never`, где он возникает и как включает проверку исчерпания?
Итог

any, unknown и never — углы системы типов: лазейка, безопасный верх и пустой низ. Нить, связывающая их — сужение (narrowing — сужение типа до более конкретного через проверку): unknown становится полезен, лишь когда вы его сужаете, а never — буквально то, что остаётся, когда сужение исчерпывает объединение. Это весь предмет урока о сужении в следующем юните (ваша цель deepensInto): typeof, in, instanceof, размеченные объединения и пользовательские защитники типов. А перед этим завершающий урок основ — литеральные типы — подхватывает нить расширения из вывода и показывает, как as const и объединения литералов дают точные, похожие на enum типы, не отказываясь от структурной типизации. Теперь, когда увидите any в сигнатуре граничного типа — распарсенный JSON, результат библиотеки, переменная catch — отнеситесь к нему как к незапертой машине на парковке: технически доступно, но проблема, которая ждёт своего часа.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.