Migrar JavaScript para TypeScript com IA: plano seguro

Como migrar JavaScript para TypeScript com agentes de IA: tsconfig que aceita JS, ordem arquivo a arquivo, rigor gradual, orçamento de any e tsc como verificação.

Solviera Teknoloji 14 min de leitura

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 tsc mostra 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:

  • allowJs permite que arquivos .js e .ts vivam no mesmo programa, então um arquivo .ts pode importar um .js e vice-versa.
  • checkJs: false impede 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, o strict vem como true por 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 o types padrã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.
  • noEmit serve para projetos em que um bundler, o tsx ou um framework já transforma o código em JavaScript e o TypeScript só verifica. Se o tsc faz o build do seu app, remova essa opção e defina outDir.
  • module e moduleResolution: use nodenext para 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:

  1. noImplicitAny: todo parâmetro e variável precisa ter um tipo conhecido. Essa gera mais erros e os tipos mais úteis.
  2. strictNullChecks: null e undefined viram tipos próprios. Essa encontra bugs reais, do tipo “cannot read property of undefined”.
  3. O resto da família strict: strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables. Geralmente menos erros cada uma.
  4. "strict": true substitui as flags individuais quando todas passam.
  5. Extras opcionais que não fazem parte do strict: noUncheckedIndexedAccess (leituras por índice podem ser undefined) e exactOptionalPropertyTypes.

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.js e as declarações compartilhadas em src/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 User ou Order, faça disso um passo sequencial antes de os paralelos começarem. Senão, três agentes escrevem três interfaces User ligeiramente diferentes.
  • Instale os pacotes @types de 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 tsc separadamente 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 como ts-jest ou babel-jest com 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 any em 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 require em import muda a semântica. Exports default, module.exports = fn e 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, um if reordenado. Cada um pode mudar o comportamento. Rejeite e peça um commit separado.
  • Silenciar o compilador pela configuração. O skipLibCheck não tem problema; desligar uma flag strict para uma branch passar, sim. É por isso que os agentes não podem editar o tsconfig.json.
  • Arquivos gerados ou de terceiros. Exclua-os no tsconfig.json em 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.json e src/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.

Perguntas frequentes

A IA consegue converter um projeto JavaScript para TypeScript?

Sim, e é um dos trabalhos em que os agentes vão bem, desde que você dê a eles um type checker funcionando, uma ordem fixa e passos pequenos. Deixe o agente converter um arquivo ou pasta por vez, rodar o tsc e os testes depois de cada passo e fazer um commit separado para cada um.

Devo ativar o strict mode desde o início?

Não numa base de código existente. Desde o TypeScript 6.0 o strict vem ligado por padrão, então defina "strict": false explicitamente no começo, converta os arquivos e depois ative noImplicitAny, strictNullChecks e as outras flags strict uma de cada vez.

Como impeço o agente de usar any em todo lugar?

Torne isso mensurável. Reporte any explícito com a regra no-explicit-any do typescript-eslint, proíba @ts-ignore com ban-ts-comment, mantenha um número de orçamento no repositório e faça o CI falhar quando a contagem passar dele. Informe o orçamento nas instruções do agente.

Vários agentes podem migrar pastas diferentes ao mesmo tempo?

Sim, se cada um trabalhar no próprio git worktree e for dono de um diretório. Arquivos compartilhados como tsconfig.json, package.json e declarações de tipos globais devem pertencer a um único agente, e cada branch deve passar pelo type check de novo depois do merge.

Preciso reescrever meus testes durante a migração?

Não. Primeiro mantenha os testes existentes rodando contra o código convertido; são eles que mostram que o comportamento não mudou. Converta os arquivos de teste depois do módulo que eles cobrem e evite mudar asserções no mesmo passo.

Reúna seus agentes em uma só mesa.

Baixe o AgentVera de graça; as CLIs que você já instalou estão prontas para usar.

Mais artigos