Para migrar JavaScript para TypeScript com IA, você não pede a um agente para “converter o projeto”. Você monta uma configuração de TypeScript que aceita o JavaScript que já existe e deixa os agentes converterem arquivos numa ordem fixa, com o type checker e os testes decidindo quando cada passo terminou. O compilador é o melhor revisor que você pode dar a um agente: é rápido, nunca se cansa e não dá para convencê-lo de que “parece ok”.
Este guia é a versão prática dessa ideia: o tsconfig.json exato para começar, como escolher a ordem dos arquivos, como apertar o rigor sem uma semana de builds vermelhos, como manter o any sob controle, como fazer do type check a etapa de verificação do próprio agente e como dividir o trabalho entre agentes em paralelo sem conflitos de merge. Para a rede de segurança geral de uma refatoração de código legado (testes de caracterização, rollback, revisão), leia antes refatorar código legado com IA; este post não repete esse conteúdo.
Os detalhes que dependem de versão seguem a documentação oficial do TypeScript em outubro de 2026.
Por que os agentes são bons nesse trabalho
Uma migração de JS para TS é, na maior parte, trabalho mecânico com um sinal claro no fim de cada passo. É exatamente aquilo em que agentes de código são bons:
- O feedback é instantâneo. O
tscmostra arquivo, linha e código de erro. Um agente consegue ler isso e corrigir sem chutar. - O trabalho se divide naturalmente. Arquivos e pastas podem ser convertidos um de cada vez, então o trabalho se quebra em muitos passos pequenos e revisáveis.
- A parte chata é enorme. Adicionar tipos de parâmetros, escrever interfaces para objetos que já existem e instalar pacotes
@typesé o tipo de trabalho que as pessoas adiam por anos.
O risco é igualmente claro. Um agente pressionado a deixar o compilador verde vai recorrer a any, casts com as e @ts-ignore, porque eles fazem os erros sumirem. A maior parte deste guia é sobre fechar essas saídas.
Passo 1: um tsconfig que aceita o seu JavaScript
O primeiro commit não deve mudar nenhum código da aplicação. Ele adiciona o TypeScript e uma configuração que faz o type check do projeto enquanto todos os arquivos ainda são .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"]
}
O que cada escolha faz:
allowJspermite que arquivos.jse.tsvivam no mesmo programa, então um arquivo.tspode importar um.jse vice-versa.checkJs: falseimpede o TypeScript de reportar erros em arquivos em que você ainda não mexeu. Você vai incluir os arquivos um a um.strict: falseé proposital. Desde o TypeScript 6.0, ostrictvem comotruepor padrão, então deixá-lo de fora ligaria todas as verificações strict no primeiro dia.types: ["node"]importa pelo mesmo motivo: no TypeScript 6.0 otypespadrão é um array vazio, então os globais do Node não são carregados se você não os listar. Adicione"jest"ou"vitest/globals"se os seus testes dependem de globais.noEmitserve para projetos em que um bundler, otsxou um framework já transforma o código em JavaScript e o TypeScript só verifica. Se otscfaz o build do seu app, remova essa opção e definaoutDir.moduleemoduleResolution: usenodenextpara apps Node; para código que um bundler compila,"module": "esnext"com"moduleResolution": "bundler"costuma ser o par certo.
Adicione um script e faça o CI rodá-lo:
{
"scripts": {
"typecheck": "tsc -p tsconfig.json"
}
}
Rode uma vez. Numa base JavaScript com o checkJs desligado, ele deve passar ou mostrar só alguns erros de configuração. Corrija esses erros, faça o commit e você tem uma linha de base. Daqui em diante, “o build está verde” significa que npm run typecheck e npm test passam.
Passo 2: veja os erros com @ts-check antes de renomear
Antes de renomear um arquivo, você pode ver o que o TypeScript acha dele. Adicione // @ts-check como primeira linha de um arquivo .js e o TypeScript reporta os erros dele, usando comentários JSDoc como tipos:
// @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);
}
Esse é um bom primeiro trabalho para um agente num arquivo grande: “Adicione // @ts-check a src/orders/pricing.js, corrija os erros só com tipos JSDoc, não mude o comportamento.” Você ganha segurança de tipos sem renomear, e a conversão posterior fica quase mecânica. Se preferir o caminho oposto, defina checkJs: true e coloque // @ts-nocheck nos arquivos que ainda não estão prontos; aí remover essa linha é o passo.
Passo 3: escolha a ordem, folhas primeiro
Converta primeiro os arquivos que não importam nada do seu próprio código, depois os arquivos que só importam esses, e assim por diante até os pontos de entrada. Assim, cada arquivo que você converte pode se apoiar em tipos que já existem.
Você não precisa descobrir a ordem na mão. O madge lê o grafo de imports:
npx madge --leaves --extensions js,jsx src/
npx madge --circular --extensions js,jsx src/
O --leaves lista módulos sem dependências, e o --circular lista ciclos de import. Vale conhecer os ciclos logo de cara: arquivos num ciclo precisam ser convertidos juntos, então planeje-os como um único passo.
Uma regra prática de agrupamento: um passo é um arquivo grande ou uma pasta de arquivos pequenos, pequeno o bastante para o diff caber numa sessão de revisão. Coloque a lista num arquivo versionado como MIGRATION.md, uma linha por passo, e deixe os agentes irem marcando. Esse arquivo vira o plano compartilhado de todos os agentes e todas as sessões.
Passo 4: um prompt de conversão, uma definição de pronto
Use o mesmo formato de prompt em todo passo, para que os agentes não inventem regras próprias:
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.
Duas linhas desse prompt fazem a maior parte do trabalho. “Não mude o comportamento em runtime” mantém o diff revisável, porque uma migração só de tipos deve adicionar apenas anotações, renomeações e imports. “Não edite o tsconfig.json” bloqueia o caminho mais barato para o verde: afrouxar o compilador.
Coloque as partes estáveis desse prompt no CLAUDE.md ou no AGENTS.md para que toda sessão comece com elas; CLAUDE.md e AGENTS.md explica como compartilhar um único conjunto de regras entre agentes.
Passo 5: um orçamento de any que só diminui
Você não vai chegar a zero any numa passada só, e proibi-lo de vez costuma gerar saídas piores, como as unknown as Foo. Um orçamento é mais honesto: você permite um número, e o número só diminui.
Primeiro torne visíveis o any e os comentários de supressão com o 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,
}],
}
O ban-ts-comment com essas opções rejeita @ts-ignore e @ts-nocheck e só aceita @ts-expect-error com um motivo escrito de pelo menos dez caracteres. O @ts-expect-error é a melhor saída de qualquer forma: quando o erro é corrigido, o próprio comentário vira um erro, então ele não fica esquecido.
Depois conte os any explícitos e compare com um número guardado no repositório:
#!/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; }
Rode isso no CI e na definição de pronto do agente. Quando um passo baixa a contagem, reduza o .any-budget no mesmo commit. Diga ao agente onde o orçamento fica; os agentes respeitam um número muito melhor do que “evite any sempre que possível”.
Fique de olho nas outras saídas, que a contagem não pega: casts com as em valores que vieram de fora (JSON.parse, corpos de requisição), Function e object como tipos de parâmetro, e uma linha declare module '*' que transforma todo import em any. Peça ao revisor, humano ou agente, para listar cada cast novo no diff.
Passo 6: faça do type check a etapa de verificação do agente
Um agente que diz “pronto” sem rodar o tsc está chutando. Faça a verificação parte de como o agente encerra um turno, não algo que você precisa lembrar de pedir.
No Claude Code, um hook Stop pode impedir o fim do turno enquanto o type check falhar. Salve isto como .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
e registre em .claude/settings.json:
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": ".claude/hooks/typecheck.sh" }
]
}
]
}
}
Quando um hook Stop sai com código 2, o Claude Code não para e mostra o stderr do hook ao Claude, então o agente vê os erros do compilador e continua corrigindo. A verificação de stop_hook_active deixa o turno terminar na segunda tentativa em vez de entrar em loop. O guia de hooks do Claude Code aprofunda os hooks. Um hook PostToolUse em Edit|Write pode rodar uma verificação mais rápida depois de cada edição, mas em projetos grandes um tsc completo por edição fica lento.
O Codex e o Gemini CLI não leem os hooks do Claude, então coloque a mesma regra no AGENTS.md ou no GEMINI.md: “Antes de dizer que uma tarefa terminou, rode npm run typecheck e npm test e cole as últimas linhas da saída.” Pedir a saída é o que faz a instrução pegar.
Passo 7: aumente o rigor uma flag de cada vez
Quando todos os arquivos forem .ts, ou uma área inteira for, ative as verificações strict nesta ordem, com um commit e uma tarefa de agente separados para cada uma:
noImplicitAny: todo parâmetro e variável precisa ter um tipo conhecido. Essa gera mais erros e os tipos mais úteis.strictNullChecks:nulleundefinedviram tipos próprios. Essa encontra bugs reais, do tipo “cannot read property of undefined”.- O resto da família strict:
strictFunctionTypes,strictBindCallApply,strictPropertyInitialization,noImplicitThis,useUnknownInCatchVariables. Geralmente menos erros cada uma. "strict": truesubstitui as flags individuais quando todas passam.- Extras opcionais que não fazem parte do
strict:noUncheckedIndexedAccess(leituras por índice podem serundefined) eexactOptionalPropertyTypes.
Você não precisa ligar uma flag para o repositório inteiro de uma vez. Uma segunda configuração que estende a primeira e cobre só as pastas terminadas permite que o código strict continue strict enquanto o resto alcança:
{
"extends": "./tsconfig.json",
"compilerOptions": { "strict": true },
"include": ["src/billing", "src/utils"]
}
Salve como tsconfig.strict.json e rode tsc -p tsconfig.strict.json no CI ao lado da verificação principal. Cada vez que um agente termina uma pasta, ele a adiciona ao include. Quando o include cobrir src, mova o strict: true para a configuração principal e apague o segundo arquivo.
Um cuidado com o strictNullChecks: a correção mais rápida para “possibly undefined” é a non-null assertion !. Algumas não fazem mal; dezenas significam que o agente está escondendo o problema. Peça uma guarda, um valor padrão ou uma mudança de tipo em vez disso, e trate cada ! novo como um any novo na revisão.
Passo 8: divida o trabalho entre agentes em paralelo
Uma migração é um dos poucos trabalhos em que rodar vários agentes ao mesmo tempo compensa de verdade, porque as pastas são bastante independentes. O jeito de fazer funcionar é dividir por diretório e dar a cada agente o próprio 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
Inicie um agente em cada pasta com o prompt de passo acima, apontado para o diretório dele. Algumas regras evitam que as branches colidam:
- Arquivos compartilhados têm um único dono.
tsconfig.json,package.json, o lockfile,eslint.config.jse as declarações compartilhadas emsrc/types/só mudam na branch principal, por você ou por um agente coordenador. Os agentes das pastas pedem mudanças ali em vez de fazê-las. - Os tipos compartilhados vêm primeiro. Se várias pastas precisam do mesmo tipo
UserouOrder, faça disso um passo sequencial antes de os paralelos começarem. Senão, três agentes escrevem três interfacesUserligeiramente diferentes. - Instale os pacotes
@typesde forma centralizada. Duas branches que adicionam pacotes diferentes reescrevem o lockfile e entram em conflito toda vez. - Não paralelize um ciclo. Pastas que se importam em círculo pertencem a um único agente.
- Faça o merge de uma branch por vez e rode o type check depois de cada merge. Duas branches podem passar no
tscseparadamente e falhar juntas, por exemplo quando uma muda um tipo exportado de que a outra depende. Faça o rebase da próxima branch sobre a nova main antes do merge.
Git worktrees para agentes em paralelo detalha a configuração e a limpeza. Três ou quatro agentes é um começo sensato; além disso, revisar as branches costuma virar o gargalo, não escrevê-las.
Passo 9: deixe os testes provarem que nada mudou
Os tipos dizem que o código é coerente consigo mesmo. Só os testes dizem que ele ainda faz o que fazia. Durante uma migração:
- Rode a suíte inteira depois de cada passo, não só os testes da pasta convertida. Um export alterado pode quebrar quem o chama em outro lugar.
- Converta os arquivos de teste depois do código que eles testam, num passo separado, e não mude asserções durante a conversão. Um teste cujo valor esperado mudou num commit “só de tipos” é um sinal de alerta.
- Confira se o test runner lida com
.ts. O Vitest roda arquivos TypeScript sem configuração extra; o Jest precisa de um transform comots-jestoubabel-jestcom o preset de TypeScript. - Adicione testes onde os tipos não alcançam. Código que faz parsing de entrada, conversa com uma API ou lê variáveis de ambiente recebe dados com cara de
anyem runtime, digam os tipos o que disserem. Alguns testes ali valem mais que anotações perfeitas. Escrever testes unitários com IA mostra como fazer os agentes escreverem testes úteis.
Armadilhas comuns
- Converter
requireemimportmuda a semântica. Exports default,module.exports = fne a interoperabilidade diferem entre CommonJS e ES modules. Deixe a mudança de formato de módulo como um passo próprio, ou fora da migração. - Tipos que mentem. Uma interface escrita a partir de um único objeto de exemplo pode perder campos que só aparecem às vezes. Prefira tipos escritos a partir de dados reais ou do schema da API.
- “Correções” de comportamento escondidas num commit de tipos.
==virando===, um parâmetro padrão adicionado, umifreordenado. Cada um pode mudar o comportamento. Rejeite e peça um commit separado. - Silenciar o compilador pela configuração. O
skipLibChecknão tem problema; desligar uma flag strict para uma branch passar, sim. É por isso que os agentes não podem editar otsconfig.json. - Arquivos gerados ou de terceiros. Exclua-os no
tsconfig.jsonem vez de deixar um agente “tipá-los”.
Fazendo isso com o AgentVera
O AgentVera roda Claude Code, Codex, Gemini CLI e outros agentes de CLI lado a lado por projeto, o que combina com uma migração dividida em muitos passos pequenos.
- Um worktree por pasta. No painel Git, crie um worktree para cada diretório e inicie um agente nele com um clique, Claude Code numa pasta e Codex em outra, se quiser.
- File ownership (posse de arquivos). Dê ao agente coordenador
tsconfig.json,package.jsonesrc/types/**. Enquanto ele roda, as edições de outros agentes nesses arquivos são recusadas antes de acontecer e eles são orientados a pedir ao dono; agentes Claude Code são barrados por um hook, outras CLIs recebem a regra nas instruções. - Verify. Escreva “o typecheck passa, os testes passam, nenhuma asserção de teste mudou” na sua mensagem. Quando o turno termina, um verificador separado que não consegue editar arquivos roda os comandos de teste permitidos, lê o diff e marca cada critério como aprovado ou reprovado, com evidências.
- Code review. Um segundo modelo lê cada turno concluído e lista os apontamentos com arquivo:linha, um bom lugar para pegar casts novos e asserções
!. - Merge queue (fila de merge). As branches das pastas entram uma por vez sobre a main mais recente, e o seu comando de verificação, como
npm run typecheck && npm test, roda no resultado do merge antes de qualquer coisa ser mesclada. - Parallel team (Pro e Team). Um agente pode dividir a migração em até oito partes, com os arquivos que cada parte pode mexer, rodar cada parte no próprio worktree, mesclar as partes concluídas numa branch de integração e rodar a verificação final, para você revisar um único pull request.
Conclusão
Migrar JavaScript para TypeScript com IA funciona quando o compilador e os testes estão no comando, não a confiança do agente. Comece com uma configuração que aceita JavaScript e define strict: false de propósito, converta as folhas primeiro em passos pequenos, mantenha o any dentro de um orçamento que só encolhe, faça do tsc a verificação de saída do agente, aumente o rigor flag por flag e divida o trabalho em paralelo por diretório, com os arquivos compartilhados sob a responsabilidade de um único agente. Cada passo é chato e revisável, que é exatamente o que você quer de uma migração.
Se você vai acompanhar os agentes passo a passo durante a migração, pair programming com IA explica quando deixar o agente conduzir e quando revisar cada passo.