Частые ошибки типизации на практике
Вопрос-крючок: «В этом коде нет ни одной красной подчёркнутой ошибки в редакторе. Значит ли это, что типизация в порядке?»
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), которые реально проверяют структуру данных в рантайме.