Кривой Typescript тип выжрал 15 гигов оперативы

Всем привет! В этой статье хочу рассказать об одном интересном(для меня) случае, который произошел со мной на прошлой неделе.
Посреди дня поступила новая задача. Я с охотой принялся вайбкодить. Для тасок такого рода довольно обычный флоу работы: верстка форм по макету, добавление новых полей в Zod схему, обновление JSX , вывод типов из Zod схемы, etc.
В процессе работы замечаю, что VSCode начал лагать: не работал вывод типов, автокомплит, подсказки.

Я уже замечал подобное около полугода назад, и в тот момент помогло отключение расширение GitHub Copilot (нашел ишью на гитхабе).
Сейчас я опять подумал на проблему с расширением и начал искать ее. Для дебага проблем в VSCode есть инструмент, который запускается через Help > Start Extension Bisect.
Он поочередно дизейблит расширения и спрашивает воспроизводится баг или нет. Таким способом, подозрения пали на расширение гитлаба. Забизейблив его, ситуация улучшилась. Вывод типов появился и автокомплит тоже.
Однако, через пару часов ситуация ухудшилась, поискав ишью на гитхабе об этой проблеме (не работающий по-сути vscode) я ничего не нашел.
Попробовал ребутнуть комп - помогло, vscode снова заработал, ура подумал я!
Но спустя какое-то время ситуация повторилась, это был вечер пятницы, конец рабочего дня, ладно, может какая-то проблема с антивирусами или еще с каким-то корпоративным ПО, которое установлено на ноуте, подумал я. Вернусь в понедельник, и все заработает
ПОНЕДЕЛЬНИК
Как полагается проблема осталась. Ребут снова помог временно, после этого вдобавок начал лагать уже и хром.
В середине рабочего дня пользоваться ноутом уже стало невыносимо. Решил открыть "Мониторинг системы". Открываю "Память" и офигеваю. При запущенных на компе Spotify, Chrome, VSCode и Time используется 28 гигов оперативы из 32 доступных!
Начинаю смотреть, что там за процессы такие: все процессы хрома в сумме занимают 6 гб, много конечно, но ок, смотрю дальше и вижу полотно запущенных node процессов. Каждый из них занимает ~700 мб оперативы. Что за процесс такой - думаю я. Открываю трейс и вижу, что запущена команда вида:
npm exec tsc --project /Users/Developer/projects/front/node_modules/.federation/tsconfig.bc0bb64cb574b1df5f6fb9bf2cef34a9.json (94412)
Она висит уже 13 минут и не может завершиться. И таких команд висят десятки:

Смотрю инициатора: какая-то либа из node_modules. По итогу понимаю, у меня запущен vite dev-сервер, и в нем либа @module-federation примерно раз в 5 мин запускает генерацию контракта для микрофронта. За час получается 12 висячих node процессов.
Окей, то что либа генерит какой-то неиспользуемый контракт в папку неиндексируемую гитом это ок, но почему генерация контракта выполняется так долго и по-сути вообще не заканчивается?
Ранее, этим же днем, давая простые таски клоду, я замечал, что он выполняет их очень долго, по 30-60 минут, задачи которые делать клоду 2 мин от силы. Заметил висячую уже 20 минут shell команду npm run check . Что-то странное подумал я и просто отменил ее.
По логике - если генерация d.ts контракта выполняется слишком долго, то логично попробовать найти ошибку в типах. Это я и начал делать. Однако явных проблем косаемо типов я не заметил.
Тогда на ум приходит посмотреть всю цепочку того как TS собирает контракт, я запустил ту же самую команду с флагом --generateTrace и заметил что компилятор упёрся в предел глубины инстанцирования типов и продолжал пересчитывать одну и ту же пару типов больше 500 000 раз подряд, не завершаясь:

И происходит это на одном конкретном файле, назовем его useSomeForm.tsx. На первый взгляд ничего подозрительно - в нем описывается форма в связке React Hook Form + Zod. При детальном рассмотрении замечаю корень проблемы:
// useSomeForm.tsx
const getBaseSchema = () => {
return z.object({
// fields here
})
}
export type BaseSchemaInferred = ReturnType<typeof getBaseSchema>;В другом файле:
// SomeForm.tsx
const { control } = useFormContext<BaseSchemaInferred>();То есть получалась такая цепочка: React Hook Form под капотом рекурсивно строит тип Path (все возможные строки‑пути до полей формы), обходя каждое поле T. Когда вместо простого объекта я ему подставил весь внутренний тип Zod‑схемы (с её union'ами.or(),.extend() и собственной рекурсивной generic‑структурой), два рекурсивных generic‑механизма (zod ↔ React Hook Form) начинали бесконечно вызывать друг друга.
Корректный вариант должен был выглядеть следующим образом:
export type BaseSchemaInferred = z.infer<BaseSchema>;В большом файле, который описывает множество полей формы с валидацией и наследованием такую ошибку было сложно заметить.
Подводя итог хочется сделать вывод о том что нужно быть внимательное и вдумчивее читать тот код, который пишет ИИ.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.