Чтобы перевести JavaScript на TypeScript с ИИ, не нужно просить агента «сконвертировать проект». Вы настраиваете конфиг TypeScript, который принимает уже существующий JavaScript, а затем агенты переводят файлы в фиксированном порядке, и готовность каждого шага определяют проверка типов и тесты. Компилятор — лучший ревьюер, которого вы можете дать агенту: он быстрый, никогда не устаёт и его невозможно уговорить на «вроде нормально».
Это руководство — практическая версия этой идеи: какой именно tsconfig.json взять для старта, как выбрать порядок файлов, как ужесточать строгость без недели красных сборок, как держать any под контролем, как сделать проверку типов собственным шагом проверки агента и как разделить работу между параллельными агентами без конфликтов слияния. Об общей страховке при рефакторинге legacy-кода (характеризационные тесты, откат, ревью) сначала прочитайте рефакторинг legacy-кода с ИИ; здесь это не повторяется.
Детали, зависящие от версии, приведены по официальной документации TypeScript на октябрь 2026 года.
Почему агенты хорошо справляются с этой работой
Миграция с JS на TS — в основном механическая работа с ясным сигналом в конце каждого шага. Именно в этом кодинг-агенты сильны:
- Обратная связь мгновенная.
tscвыводит файл, строку и код ошибки. Агент может прочитать это и исправить без догадок. - Работа естественно делится. Файлы и папки можно переводить по одному, так что задача распадается на множество мелких шагов, которые легко проверить.
- Скучной части очень много. Добавить типы параметров, написать интерфейсы для уже существующих объектов и поставить пакеты
@types— это та работа, которую люди откладывают годами.
Риск так же очевиден. Агент, которого торопят сделать компилятор зелёным, потянется к any, приведениям as и @ts-ignore, потому что они заставляют ошибки исчезнуть. Большая часть этого руководства — о том, как закрыть эти лазейки.
Шаг 1: tsconfig, который принимает ваш JavaScript
Первый коммит не должен менять код приложения. Он добавляет TypeScript и конфиг, который проверяет типы в проекте, пока все файлы ещё .js.
npm install --save-dev typescript @types/node
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"strict": false,
"noEmit": true,
"incremental": true,
"target": "es2022",
"module": "nodenext",
"moduleResolution": "nodenext",
"types": ["node"],
"skipLibCheck": true
},
"include": ["src"]
}
Что делает каждая настройка:
allowJsпозволяет файлам.jsи.tsжить в одной программе, так что файл.tsможет импортировать файл.jsи наоборот.checkJs: falseне даёт TypeScript сообщать об ошибках в файлах, которых вы ещё не касались. Вы будете подключать файлы по одному.strict: falseзадан намеренно. Начиная с TypeScript 6.0strictпо умолчанию равенtrue, так что если его не указать, все строгие проверки включатся в первый же день.types: ["node"]важен по той же причине: в TypeScript 6.0typesпо умолчанию — пустой массив, поэтому глобальные объекты Node не подхватываются, пока вы их не перечислите. Добавьте"jest"или"vitest/globals", если ваши тесты полагаются на глобальные объекты.noEmitподходит проектам, где код в JavaScript уже превращает сборщик,tsxили фреймворк, а TypeScript только проверяет его. Если ваше приложение собираетtsc, уберите его и задайтеoutDir.moduleиmoduleResolution: для приложений на Node используйтеnodenext; для кода, который собирает сборщик, обычно правильная пара —"module": "esnext"с"moduleResolution": "bundler".
Добавьте один скрипт и пусть CI его запускает:
{
"scripts": {
"typecheck": "tsc -p tsconfig.json"
}
}
Запустите его один раз. На кодовой базе JavaScript с выключенным checkJs он должен пройти или показать лишь несколько ошибок конфига. Исправьте их, закоммитьте, и у вас есть отправная точка. С этого момента «сборка зелёная» означает, что проходят и npm run typecheck, и npm test.
Шаг 2: посмотрите ошибки через @ts-check до переименования
Ещё до переименования файла можно увидеть, что о нём думает TypeScript. Добавьте // @ts-check первой строкой в файл .js, и TypeScript покажет в нём ошибки, используя JSDoc-комментарии как типы:
// @ts-check
/**
* @param {number} cents
* @param {string} currency
* @returns {string}
*/
export function formatPrice(cents, currency) {
return new Intl.NumberFormat('en', { style: 'currency', currency }).format(cents / 100);
}
Это хорошая первая задача для агента на большом файле: «Добавь // @ts-check в src/orders/pricing.js, исправь ошибки только типами JSDoc, не меняй поведение». Вы получаете типобезопасность без переименования, а последующий перевод становится почти механическим. Если вам ближе обратный подход, задайте checkJs: true и поставьте // @ts-nocheck в файлы, которые ещё не готовы; тогда шагом становится удаление этой строки.
Шаг 3: выберите порядок, сначала листья
Сначала переводите файлы, которые ничего не импортируют из вашего собственного кода, затем файлы, которые импортируют только их, и так далее до точек входа. Так каждый переводимый файл может опираться на уже существующие типы.
Выстраивать порядок вручную не нужно. madge читает граф импортов:
npx madge --leaves --extensions js,jsx src/
npx madge --circular --extensions js,jsx src/
--leaves выводит модули без зависимостей, а --circular — циклы импортов. О циклах стоит знать заранее: файлы в цикле приходится переводить вместе, поэтому планируйте их как один шаг.
Практичное правило для разбивки: один шаг — это либо один большой файл, либо одна папка мелких файлов, достаточно маленькая, чтобы дифф можно было проверить за один присест. Запишите список в файл в репозитории, например MIGRATION.md, по строке на шаг, и пусть агенты отмечают выполненное. Этот файл становится общим планом для каждого агента и каждой сессии.
Шаг 4: один промпт для перевода, одно определение готовности
Используйте одну и ту же форму промпта для каждого шага, чтобы агенты не придумывали собственные правила:
Convert src/billing/ from JavaScript to TypeScript.
- Rename with git mv so history follows the files (.js -> .ts, .jsx -> .tsx).
- Add types for parameters, return values and exported objects.
Prefer types inferred from real usage over wide ones.
- Don't change runtime behaviour. No refactoring, no renaming,
no "while I'm here" fixes. If you find a bug, note it in
MIGRATION.md under "Found during migration" and leave it.
- No @ts-ignore, no @ts-nocheck. @ts-expect-error only with a reason
on the same line. No new `any` beyond the budget in .any-budget.
- Don't edit tsconfig.json, package.json or src/types/.
If you need a change there, stop and tell me what and why.
Done means: npm run typecheck passes, npm test passes,
npm run lint passes, and MIGRATION.md is updated.
Большую часть работы в этом промпте делают две строки. «Не меняй поведение во время выполнения» держит дифф проверяемым, потому что чистая миграция типов должна добавлять только аннотации, переименования и импорты. «Не редактируй tsconfig.json» перекрывает самый дешёвый путь к зелёной сборке: ослабить компилятор.
Положите стабильные части этого промпта в CLAUDE.md или AGENTS.md, чтобы каждая сессия начиналась с них; как разделить один набор правил между агентами, объясняет статья CLAUDE.md и AGENTS.md.
Шаг 5: бюджет any, который только уменьшается
За один проход до нуля any не дойти, а полный запрет обычно порождает лазейки похуже, вроде as unknown as Foo. Бюджет честнее: вы разрешаете некоторое число, и это число только уменьшается.
Сначала сделайте any и комментарии-заглушки видимыми с помощью typescript-eslint:
// eslint.config.js (excerpt)
rules: {
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/ban-ts-comment': ['error', {
'ts-expect-error': 'allow-with-description',
'ts-ignore': true,
'ts-nocheck': true,
minimumDescriptionLength: 10,
}],
}
ban-ts-comment с этими настройками отклоняет @ts-ignore и @ts-nocheck и принимает @ts-expect-error только с письменным объяснением длиной не меньше десяти символов. @ts-expect-error в любом случае лучшая лазейка: как только ошибку исправят, сам комментарий становится ошибкой, так что он не может устареть незаметно.
Затем посчитайте явные any и сравните с числом, которое хранится в репозитории:
#!/usr/bin/env bash
# scripts/any-budget.sh
count=$(npx eslint src --format json \
| node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const r=JSON.parse(s);console.log(r.flatMap(f=>f.messages).filter(m=>m.ruleId==="@typescript-eslint/no-explicit-any").length)})')
budget=$(cat .any-budget)
echo "explicit any: $count (budget $budget)"
[ "$count" -le "$budget" ] || { echo "any budget exceeded"; exit 1; }
Запускайте его в CI и включите в определение готовности для агента. Когда шаг уменьшает счётчик, снижайте .any-budget в том же коммите. Скажите агенту, где лежит бюджет: число агенты соблюдают гораздо лучше, чем «по возможности избегай any».
Следите и за другими лазейками, которые счётчик не ловит: приведения as для значений, пришедших извне (JSON.parse, тела запросов), Function и object как типы параметров и строка declare module '*', которая превращает каждый импорт в any. Попросите ревьюера, человека или агента, перечислить каждое новое приведение в диффе.
Шаг 6: сделайте проверку типов шагом проверки агента
Агент, который говорит «готово», не запустив tsc, гадает. Сделайте проверку частью того, как агент завершает ход, а не тем, о чём вы должны не забыть попросить.
В Claude Code хук Stop может не дать ходу завершиться, пока проверка типов не проходит. Сохраните это как .claude/hooks/typecheck.sh:
#!/usr/bin/env bash
# Exit 2 sends stderr back to Claude and keeps it working.
input=$(cat)
if [ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # already retried once; let the turn end
fi
npx tsc -p tsconfig.json --pretty false 1>&2 || exit 2
и зарегистрируйте его в .claude/settings.json:
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": ".claude/hooks/typecheck.sh" }
]
}
]
}
}
Когда хук Stop завершается с кодом 2, Claude Code не останавливается и показывает Claude вывод stderr хука, так что агент видит ошибки компилятора и продолжает исправлять. Проверка stop_hook_active позволяет ходу завершиться со второй попытки, а не зациклиться. Подробнее о хуках — в руководстве по хукам Claude Code. Хук PostToolUse на Edit|Write может запускать более быструю проверку после каждой правки, хотя на больших проектах полный tsc после каждой правки становится медленным.
Codex и Gemini CLI не читают хуки Claude, поэтому запишите то же правило в AGENTS.md или GEMINI.md: «Прежде чем сказать, что задача готова, запусти npm run typecheck и npm test и вставь последние строки вывода». Именно требование показать вывод заставляет инструкцию работать.
Шаг 7: повышайте строгость по одному флагу
Когда все файлы стали .ts (или все файлы одной области), включайте строгие проверки в таком порядке, с отдельным коммитом и отдельной задачей для агента на каждую:
noImplicitAny: у каждого параметра и переменной должен быть известный тип. Этот флаг даёт больше всего ошибок и самые полезные типы.strictNullChecks:nullиundefinedстановятся отдельными типами. Этот флаг находит настоящие баги вида «cannot read property of undefined».- Остальное семейство strict:
strictFunctionTypes,strictBindCallApply,strictPropertyInitialization,noImplicitThis,useUnknownInCatchVariables. Обычно у каждого меньше ошибок. "strict": trueзаменяет отдельные флаги, когда все они проходят.- Необязательные дополнения, не входящие в
strict:noUncheckedIndexedAccess(чтение по индексу может вернутьundefined) иexactOptionalPropertyTypes.
Включать флаг сразу для всего репозитория не обязательно. Второй конфиг, который расширяет первый и охватывает только готовые папки, позволяет строгому коду оставаться строгим, пока остальной догоняет:
{
"extends": "./tsconfig.json",
"compilerOptions": { "strict": true },
"include": ["src/billing", "src/utils"]
}
Сохраните его как tsconfig.strict.json и запускайте tsc -p tsconfig.strict.json в CI рядом с основной проверкой. Каждый раз, когда агент заканчивает папку, он добавляет её в include. Когда include охватывает src, перенесите strict: true в основной конфиг и удалите второй файл.
Предостережение насчёт strictNullChecks: самый быстрый способ исправить «possibly undefined» — non-null assertion !. Несколько штук — нормально; десятки означают, что агент прячет проблему. Просите вместо этого проверку, значение по умолчанию или изменение типа, и на ревью относитесь к новому ! так же, как к новому any.
Шаг 8: разделите работу между параллельными агентами
Миграция — одна из немногих задач, где запуск нескольких агентов одновременно действительно окупается, потому что папки в основном независимы. Чтобы это сработало, делите по папкам и дайте каждому агенту свой git worktree:
git worktree add ../app-ts-billing -b ts/billing
git worktree add ../app-ts-orders -b ts/orders
git worktree add ../app-ts-users -b ts/users
Запустите по агенту в каждой папке с промптом для шага, приведённым выше, нацелив его на свою папку. Несколько правил не дают веткам столкнуться:
- У общих файлов один владелец.
tsconfig.json,package.json, lock-файл,eslint.config.jsи общие объявления вsrc/types/меняются только в основной ветке — вами или одним координирующим агентом. Агенты папок просят изменить их, а не меняют сами. - Общие типы идут первыми. Если нескольким папкам нужен один и тот же тип
UserилиOrder, сделайте его последовательным шагом до начала параллельных. Иначе три агента напишут три немного разных интерфейсаUser. - Ставьте пакеты
@typesцентрализованно. Две ветки, каждая из которых добавляет свой пакет, переписывают lock-файл и каждый раз конфликтуют. - Не распараллеливайте цикл. Папки, которые импортируют друг друга по кругу, принадлежат одному агенту.
- Сливайте по одной ветке и проверяйте типы после каждого слияния. Две ветки могут по отдельности проходить
tscи вместе падать, например когда одна меняет экспортируемый тип, на который опирается другая. Перед слиянием следующей ветки сделайте ей rebase на новый main.
Настройка и уборка подробно описаны в статье Git worktree для параллельных агентов. Три-четыре агента — разумное начало; дальше узким местом обычно становится не написание веток, а их проверка.
Шаг 9: пусть тесты докажут, что ничего не изменилось
Типы говорят, что код согласован сам с собой. Только тесты говорят, что он по-прежнему делает то, что делал. Во время миграции:
- Запускайте весь набор тестов после каждого шага, а не только тесты переведённой папки. Изменённый экспорт может сломать вызывающий код в другом месте.
- Переводите файлы тестов после кода, который они тестируют, отдельным шагом, и не меняйте утверждения при переводе. Тест, у которого ожидаемое значение изменилось в коммите «только типы», — тревожный сигнал.
- Проверьте, что тест-раннер понимает
.ts. Vitest запускает файлы TypeScript без дополнительной настройки; Jest нужен трансформер вродеts-jestилиbabel-jestс пресетом TypeScript. - Добавляйте тесты там, куда не дотягиваются типы. Код, который разбирает ввод, обращается к API или читает переменные окружения, во время выполнения получает данные формы
any, что бы ни говорили типы. Несколько тестов там стоят больше, чем идеальные аннотации. Как заставить агентов писать полезные тесты, показано в статье юнит-тесты с ИИ.
Частые ошибки
- Замена
requireнаimportменяет семантику. Экспорты по умолчанию,module.exports = fnи взаимодействие модулей различаются в CommonJS и ES-модулях. Делайте смену формата модулей отдельным шагом или вообще не включайте её в миграцию. - Типы, которые врут. Интерфейс, написанный по одному объекту-примеру, может упустить поля, которые появляются лишь иногда. Предпочитайте типы, написанные по реальным данным или по схеме API.
- «Исправления» поведения, спрятанные в коммите с типами.
==превратилось в===, добавлен параметр по умолчанию, переставленif. Каждое из них может изменить поведение. Отклоняйте их и просите отдельный коммит. - Заглушить компилятор конфигом.
skipLibCheck— это нормально; выключить строгий флаг, чтобы ветка прошла, — нет. Поэтому агентам и не дают редактироватьtsconfig.json. - Сгенерированные и вендорные файлы. Исключите их в
tsconfig.json, а не позволяйте агенту их «типизировать».
Как это сделать в AgentVera
AgentVera запускает Claude Code, Codex, Gemini CLI и других CLI-агентов бок о бок в каждом проекте, и это хорошо подходит для миграции, разбитой на много мелких шагов.
- Worktree на каждую папку. В панели Git создайте worktree для каждой папки и запустите в нём агента одним кликом: если хотите, Claude Code в одной папке и Codex в другой.
- Владение файлами. Отдайте координирующему агенту
tsconfig.json,package.jsonиsrc/types/**. Пока он работает, правки других агентов в этих файлах отклоняются до того, как произойдут, и агентам говорят обратиться к владельцу; агентов Claude Code останавливает хук, другие CLI получают правило в своих инструкциях. - Verify. Напишите в сообщении «проверка типов проходит, тесты проходят, ни одно утверждение в тестах не изменено». Когда ход заканчивается, отдельный верификатор, который не может править файлы, запускает разрешённые команды тестов, читает дифф и отмечает каждый критерий как пройденный или нет, с доказательством.
- Ревью кода. Вторая модель читает каждый завершённый ход и перечисляет замечания с file:line — хорошее место, чтобы поймать новые приведения и утверждения
!. - Очередь слияния. Ветки папок попадают по одной на свежий main, и ваша проверочная команда, например
npm run typecheck && npm test, запускается на результате слияния, прежде чем что-то будет слито. - Параллельная команда (Pro и Team). Агент может разделить миграцию на до восьми частей с файлами, которые может трогать каждая часть, запустить каждую часть в своём worktree, слить готовые части в одну интеграционную ветку и запустить финальную проверку, так что вы проверяете один pull request.
Подводим итоги
Миграция с JavaScript на TypeScript с ИИ работает, когда главные — компилятор и тесты, а не уверенность агента. Начните с конфига, который принимает JavaScript и намеренно задаёт strict: false, переводите сначала листья мелкими шагами, держите any в бюджете, который только сокращается, сделайте tsc проверкой на выходе для агента, повышайте строгость флаг за флагом и делите параллельную работу по папкам, а общие файлы отдайте одному агенту. Каждый шаг скучен и легко проверяется — именно этого и хочется от миграции.
Если во время миграции вы будете следить за агентами шаг за шагом, статья парное программирование с ИИ объясняет, когда позволить агенту вести, а когда проверять каждый шаг.