Частые ошибки типизации на практике

Вопрос-крючок: «В этом коде нет ни одной красной подчёркнутой ошибки в редакторе. Значит ли это, что типизация в порядке?»

function processOrder(order) {
  return order.total * 1.2;
}

console.log(processOrder({ total: 100 }));
console.log(processOrder("сто рублей"));

Вывод:

120
NaN

Второй вызов передаёт строку вместо объекта с полем total — и логически это явная ошибка использования функции. Но TypeScript её не поймал, хотя вся суть TypeScript в том, чтобы ловить именно такие ошибки ДО запуска программы. Дело в параметре order без указанного типа — это и есть первая из двух главных практических ошибок раздела: неявный any.

Что происходит и почему (неявный any). Если параметр функции не имеет аннотации типа, а TypeScript не может вывести тип из контекста, он молча присваивает параметру тип any. Тип any — это полное отключение проверок: с переменной типа any можно делать что угодно — обращаться к любым полям, вызывать любые методы, передавать её куда угодно — и компилятор никогда не пожалуется. По сути any — это «дыра» в системе типов, через которую в типизированный код просачивается поведение обычного нетипизированного JavaScript.

В нашем примере order без типа получает any, поэтому order.total тоже any, и умножение any * 1.2 компилятор пропускает без единого замечания — хотя в рантайме строка, умноженная на число, даёт NaN (Not a Number). Программа не упала с ошибкой, она просто вернула бессмысленное значение — а это гораздо опаснее незамеченного краша, потому что NaN может незаметно поехать дальше по всей системе (например, попасть в базу данных как итоговая сумма заказа).

Лечится это одной строкой конфигурации в tsconfig.json: флагом "noImplicitAny": true. С этим флагом компилятор ТРЕБУЕТ явно указать тип у каждого параметра, у которого не получилось вывести тип автоматически:

interface Order {
  total: number;
}

function processOrder(order: Order) {
  return order.total * 1.2;
}

processOrder({ total: 100 });   // OK
processOrder("сто рублей");     // ошибка компиляции: string не Order

Второй вызов теперь не пройдёт компиляцию вообще — ошибка находится за секунды, а не после того как в продакшене кто-то заметит NaN в чеке. Флаг noImplicitAny входит в набор строгого режима "strict": true и в большинстве серьёзных проектов включён с первого дня, потому что unknown/any в параметрах — источник огромной доли рантайм-ошибок.

Что происходит и почему (as «на всякий случай»). Вторая практическая ошибка — оператор приведения типа as, который используют не по назначению. Смысл as в том, чтобы сказать компилятору «я знаю про этот код больше, чем ты можешь вывести сам, доверься мне» — например, когда вы точно знаете структуру данных из внешнего источника, а TypeScript вывести её не может. Но на практике as часто используют, чтобы просто заставить замолчать ошибку компилятора, не разбираясь в её причине:

interface User {
  id: number;
  name: string;
  email: string;
}

function saveUser(raw: unknown) {
  const user = raw as User; // «на всякий случай», чтобы TS не ругался
  sendEmail(user.email);
}

saveUser({ id: 1, name: "Аня" }); // email отсутствует, но компилятор молчит

Здесь raw имеет тип unknown (например, это данные, только что распарсенные из JSON или пришедшие из формы). Оператор as User НЕ проверяет, что в raw реально лежит объект с полями id, name и email — он просто заставляет компилятор ПОВЕРИТЬ, что это так. Если в реальности объект пришёл без поля email (как в примере выше), TypeScript не заметит проблему — а в рантайме user.email окажется undefined, и функция sendEmail получит мусор вместо адреса.

Как это работает под капотом. Ключевая вещь, которую нужно понимать про as: это ЧИСТО КОМПИЛЯЦИОННАЯ операция. При сборке проекта TypeScript полностью стирает все типы и аннотации — в скомпилированном JavaScript от as User не остаётся вообще ничего, ни единой инструкции проверки. Это принципиально отличается, например, от приведения типов в языках с рантайм-проверками (как cast в Java, который бросает исключение ClassCastException при несовпадении). В TypeScript as — это просьба к компилятору «закрой глаза здесь», а не команда рантайму что-то реально проверить или преобразовать.

Именно поэтому as иногда называют «побегом» из системы типов (type escape hatch) — используется законно, только когда у разработчика есть внешняя гарантия правильности данных, которую TypeScript не может вывести сам (например, только что провалидировали объект вручную или через библиотеку вроде Zod). Использовать as вместо настоящей проверки — значит просто спрятать потенциальную ошибку до момента, когда она вылезет в проде.

Правильная альтернатива — реальная проверка структуры данных перед тем, как доверять типу:

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "name" in value &&
    "email" in value
  );
}

function saveUser(raw: unknown) {
  if (isUser(raw)) {
    sendEmail(raw.email); // теперь безопасно: raw реально проверен
  } else {
    console.log("Некорректные данные пользователя");
  }
}

Функция isUser — это так называемый type guard (функция-предохранитель типа), она реально проверяет наличие нужных полей в рантайме и лишь ПОСЛЕ успешной проверки TypeScript сужает тип raw до User внутри блока if. Разница с as принципиальна: type guard действительно исполняется и защищает от плохих данных, а as — это просто договорённость с компилятором, ничего не проверяющая на практике.

Частые ошибки на собеседовании. Путают as с настоящим приведением типа из других языков — думают, что as как-то преобразует или валидирует значение в рантайме, хотя это лишь подсказка компилятору, которая полностью исчезает после сборки. Забывают включить noImplicitAny в tsconfig и потом удивляются, откуда в «типизированном» проекте берутся ошибки уровня чистого JavaScript. Используют двойное приведение value as unknown as User, чтобы обойти ошибку самого as (TypeScript ругается, если типы совсем не пересекаются) — это верный признак, что где-то в цепочке типов что-то спроектировано неправильно, а не повод форсировать компилятор через unknown. И ещё одна классика — пишут any вместо unknown для действительно неизвестных данных (например, ответ внешнего API), хотя unknown безопаснее: он тоже принимает любое значение, но не даёт им пользоваться без предварительной проверки типа, в отличие от any, который снимает все ограничения сразу.

Итоги-шпаргалка. Неявный any — параметр без типа, который TypeScript не смог вывести сам, тихо получает any и выключает все проверки; лечится флагом noImplicitAny (входит в strict). as — не преобразование и не проверка, а просьба к компилятору поверить на слово; после компиляции от as не остаётся и следа в JavaScript. Для непроверенных данных используйте unknown вместо any, а вместо as пишите настоящие type guard функции (value is Type), которые реально проверяют структуру данных в рантайме.

Проверьте себя
1. Функция function calc(x) { return x * 2; } не имеет ошибок в обычном режиме TypeScript. Что произойдёт, если включить "noImplicitAny": true в tsconfig.json?
AНичего не изменится, флаг влияет только на классы
BКомпилятор выдаст ошибку, потребовав явно указать тип параметра x
CФункция автоматически станет строго типизированной без изменений в коде
DnoImplicitAny запрещает использовать умножение в функциях
2. Чем оператор as (value as User) принципиально отличается от функции-предохранителя типа (type guard, value is User)?
AНичем, это два синтаксиса для одного и того же
Bas — просьба к компилятору поверить на слово без проверки в рантайме; type guard реально проверяет структуру данных при выполнении программы
Cas работает быстрее, потому что не делает проверок
Dtype guard доступен только для примитивных типов, as — только для объектов