21 сентября 2026 года мы запустили на собственном сайте npx tsc --noEmit. Эта проверка ничего не собирает, а только проверяет типы в TypeScript-проекте. Результат: 154 ошибки.
Странно здесь не число. Странно, что ни одна из этих ошибок ни разу не остановила сборку. В tsconfig.json стояло strict: true, самый строгий режим проверки. А в next.config.mjs рядом жили две строки:
typescript: { ignoreBuildErrors: true },
eslint: { ignoreDuringBuilds: true },
Проверка выполнялась при каждой сборке, и её результат каждый раз выбрасывался. Сборка оставалась зелёной при любом количестве ошибок. У линтера в тот же день было 857 замечаний; крупнейшие группы — 547 о типе any и 300 о неиспользуемых переменных.
Ниже о том, что скрывалось внутри этих 154 ошибок, почему такие дефекты никому не мешают и как проверить, защищает ли вас ваша сборка.
Сначала решить, что считать
154 — это всё вместе. 65 ошибок были в разовых скриптах и тестах, которые в прод не попадают. Остальные 89 — в коде, который обслуживает посетителей и админку.
Поэтому первым шагом мы вынесли скрипты в отдельный tsconfig. Ошибку в скрипте, который запускали один раз для переноса данных, стоит исправить, но о состоянии сайта она ничего не говорит. К моменту этого разделения ошибок оставалось 132, и 59 из них сидели именно в скриптах. Считать нужно то, что работает в проде, иначе приоритеты размываются с первого дня.
Два дефекта, которые показывали нули
Самыми дорогими оказались не те ошибки, которые могли бы уронить страницу. Страницы работали. Проблемы сидели там, где сайт показывает цифры.
Статистика коротких ссылок в админке. Для каждой ссылки там есть карточки и графики: с каких устройств переходили, из каких стран. Все они показывали ноль. Сервер отдавал разбивку словарём, примерно так:
{ desktop: 206, mobile: 33 }
Интерфейс же ждал массив и проверял вход через Array.isArray. Словарь эту проверку не проходит, поэтому компонент решал, что данных нет, и рисовал ноль. TypeScript говорил об этом прямо: объект нельзя присвоить типу массива. Сборка это сообщение выбрасывала.
После исправления на нашей ссылке с 252 кликами появилось то, что там было всё время: 206 переходов с десктопа, 33 с мобильных, топ-страна Украина со 199 кликами.
Экспорт аналитики. Кнопки выгрузки в CSV, Excel и PDF работали, файлы скачивались, а в каждой ячейке стоял ноль. Данные хранились как словарь «период → число», а код экспорта искал в нём объекты с полем .count. У числа такого поля нет, так что каждое значение превращалось в ноль. Итоговая строка выглядела ещё красноречивее: 0[object Object]. Сумму считали, прибавляя к нулю объекты вместо чисел, и JavaScript склеил их в строку.
Для владельца сайта последствие в обоих случаях одинаковое: отчёт есть, выглядит нормально и врёт. Решение, принятое по такому отчёту, принято по нулям.
Остальные живые дефекты
Во вкладке SEO-страниц админки сортировка по колонке молча игнорировалась. Интерфейс отправлял параметры sortBy и sortOrder, а сервер ждал orderBy и orderDir. Стрелка в заголовке переключалась, список оставался в том же порядке. Человек, который на это смотрит, скорее решит, что так задумано, чем пойдёт искать расхождение в названиях параметров.
Аналитика кнопки «поделиться» писалась со сдвигом: в метку платформы попадал адрес статьи, а в адрес статьи — слово «button». Аргументы функции стояли не в том порядке.
Ещё одна мелочь касалась письма-подтверждения из контактной формы. Шаблон ждал поле «компания», которого нет ни в форме, ни в базе, поэтому строка «от компании …» в письме не появлялась никогда.
Учёт запросов, заработавший 20 сентября, имел утечку меток. Для маршрутов под /api метка бралась из сырого адреса, и боты, перебирающие адреса вроде /api/.env или /api/credentials.yml, за первые сутки дали 15 из 30 меток API. Каждый новый адрес сканера — новая серия в системе метрик навсегда. Теперь все 404 под /api сливаются в одну метку.
Самой странной находкой был модуль CDN. CDN у сайта нет, а эндпоинт без какой-либо авторизации отдавал кому угодно выдуманную статистику: «доля попаданий в кеш 81,6%» с январскими датами. Функция «очистки кеша» ничего не очищала, а лишь писала в лог Cache purged successfully. Мы удалили модуль вместе с другим кодом, который никто не вызывал, — всего около 1 300 строк.
Ловушки, которые ещё не сработали
Некоторые находки ничего не ломали только потому, что соответствующий код сейчас не выполняется.
В проекте жили две одноимённые функции учёта запросов с разным порядком аргументов. Один модуль импортировал не ту, и код статуса 200 оказывался в метке HTTP-метода. Пока этот путь не вызывается, вреда нет. Как только его подключили бы, метрики были бы перемешаны с первого же запроса.
Тесты тоже оказались декорацией: vitest не запускался вообще, потому что не хватало пакета окружения jsdom. Тестовый файл маршрута проверки состояния описывал его версию, которой давно не существовало. После исправления в проекте 67 тестов, все проходят.
Остальные ошибки относились к типу «тип врёт о рантайме». Поле есть в базе, но его нет в ручном описании типа; один и тот же тип объявлен дважды; у параметра функции вообще нет типа. Работу сайта это не ломало. Однако живые дефекты выше сидели именно в этом шуме, и добраться до них можно было, только пройдя его весь.
Почему это молчит
Зелёная сборка — самый сильный сигнал «всё в порядке», который есть в разработке. Когда ошибки игнорируются, их никто не видит, и каждая новая тонет среди старых.
Флаг ignoreBuildErrors часто появляется по вполне понятной причине: правку нужно выложить срочно, а сборка падает на чём-то постороннем. Его включают один раз, чтобы быстро задеплоить, и забывают.
Важнее другое: эти дефекты не роняли страницы. Админка показывала нули, а не ошибку. Ноль — правдоподобное значение, ведь переходов действительно могло не быть. О тихих нулях никто не сообщает. Похожий узор мы уже разбирали в истории про мониторинг, который жил только в документации: метрики объявлены, панели стоят, а данных под ними нет.
Как мы разбирали
Каждую ошибку читали как вопрос «что здесь происходит во время выполнения?», а не «как это заглушить». as any или // @ts-ignore убирают красное подчёркивание за секунду и прячут дефект надолго.
Больше всего внимания получили такие сообщения:
- «свойство не существует» (TS2339): код читает поле, которого в данных нет, а значит, всегда получает
undefined;
- «аргумент не того типа» (TS2345): часто за этим стоит перепутанный порядок аргументов, как в кнопке «поделиться»;
- «нельзя присвоить» (TS2322): данные имеют другую форму, чем ждёт интерфейс. Именно так выглядели нули в статистике ссылок.
Каждый найденный дефект проверяли отдельно, данными из базы или запуском. Исчезновение ошибки в редакторе доказательством не считали: его можно получить и неправильным исправлением.
Что изменилось 22 сентября
Когда ошибок типов в коде стало ноль, мы отключили игнорирование: ignoreBuildErrors: false. Теперь новая ошибка типов останавливает сборку и до прода не доходит.
Линтер в сборке пока отключён. Там ещё сотни замечаний про any, и остановка на нём сейчас означала бы остановку каждого деплоя. Неиспользуемые переменные убрали механически: было 291, стало 106.
Порядок шагов здесь имеет значение. Если бы мы сначала отключили игнорирование, а потом начали исправлять, первый же деплой остановился бы.
Как проверить у себя
Первое — учитывает ли сборка типы вообще:
grep -n "ignoreBuildErrors\|ignoreDuringBuilds" next.config.*
Если там true, зелёная сборка ничего не говорит о состоянии кода. Дальше посчитайте реальное количество ошибок:
npx tsc --noEmit | grep -c "error TS"
Отфильтруйте скрипты и тесты и смотрите на код приложения: он и есть сайт. Начинать стоит с трёх кодов, которые чаще всего означают настоящее расхождение между данными и кодом:
npx tsc --noEmit | grep -E "TS2339|TS2345|TS2322"
Отдельно проверьте, стартуют ли тесты: npx vitest run или npm test. Тесты, которые не запускаются, ничего не проверяют, сколько бы их ни было в репозитории.
Остановку сборки включайте только после того, как ошибок станет ноль. Иначе следующий деплой остановится, и флаг вернут обратно.
Если разработчика под рукой нет, попросите кого-нибудь выполнить две первые проверки и показать результат. Когда в конфигурации true, а ошибок десятки, цифрам в админке стоит устроить выборочную проверку: откройте статистику, правильный ответ для которой вы знаете из другого источника, и сверьте.
Границы этого разбора
Это один проект — наш сайт. Из 154 ошибок у нас нашлось семь живых дефектов и несколько ловушек, которые ещё не успели сработать. В другом проекте под теми же ошибками может оказаться больше, меньше или ничего. Количество ошибок типов не говорит, сколько под ними поломок. Оно говорит лишь о том, что сборка о них молчит.
Если проект достался вам от другого исполнителя и неизвестно, что в нём работает, а что только так выглядит, такой разбор входит в подхват заброшенного проекта. Когда конкретные поломки уже замечены, это исправление после подрядчика.
Для себя мы вынесли одно правило: проверка, результат которой выбрасывается, не лучше отсутствия проверки. Она лишь создаёт впечатление, что защита есть.