Como migrar um projeto de JavaScript para TypeScript com o Claude Code

migração de projeto JavaScript para TypeScript com Claude Code
Resposta rápida

Migrar JavaScript para TypeScript não precisa ser um big bang: dá pra fazer em fatias, com o projeto rodando o tempo todo. O caminho é gerar o tsconfig.json com tsc --init, ligar allowJs pro projeto aceitar .js e .ts juntos, ligar checkJs pra ver os erros antes de converter, renomear um arquivo por vez e rodar tsc --noEmit entre os lotes. O Claude Code entra como executor (Plan mode pra mapear a ordem, subagente por lote) e o compilador entra como juiz. Depois vem noImplicitAny, strict e a faxina das supressões

O medo de migrar um projeto pra TypeScript não é o TypeScript

É quebrar o projeto no meio do caminho e ficar naquele limbo onde nada compila e nada roda 😅

A boa notícia: essa migração não precisa ser um big bang

Dá pra fazer em fatias, com o projeto de pé entre uma etapa e outra, usando o Claude Code como executor e o compilador como juiz

Aqui vai um passo a passo de migração incremental, com o que pedir ao agente, o que rodar pra conferir e onde a conversão automática costuma esconder erro

Sem promessa de conversão mágica, beleza? Isso não existe

Se você ainda tá pegando o ritmo do agente, vale antes dar uma olhada no fluxo completo do Claude Code, que é onde esse tipo de tarefa longa se encaixa

O que ter pronto antes de pedir a primeira conversão

Antes de soltar o agente em cima do código, monta o campo de jogo

São quatro coisas, nenhuma delas demora:

  • Um tsconfig.json, gerado com tsc --init, que já vem com um conjunto das opções mais comuns listadas e comentadas
  • Um comando de checagem usando noEmit, que faz o compilador checar os tipos sem gerar nenhum arquivo de saída (nem JS, nem source map, nem declaração)
  • Controle de versão limpo, sem nada pendente, pra você conseguir voltar atrás quando um lote sair torto
  • Um CLAUDE.md com as regras da migração, porque é o arquivo de instruções persistentes que o Claude lê no início de cada sessão
Domine o Claude Code do básico ao avançado
Pré-inscrição Formação Claude Code

Domine o Claude Code do básico ao avançado

Você vai aprender a criar sistemas completos com Claude Code, sem precisar ser programador. Inscreva-se para ter acesso a um desconto de lançamento e bônus especiais!

Sobre esse último: o comando /init gera um CLAUDE.md inicial e o /memory serve pra refinar

É ali que você escreve as regras que não podem sumir quando o contexto rodar: um lote por vez, não mexer em arquivo fora do lote, rodar a checagem antes de dizer que terminou, por aí vai

Se a mesma instrução se repete em todo projeto seu, dá até pra empacotar isso e criar skills no Claude Code em vez de recolar texto

E tem um detalhe de operação que muda tudo: o Shift+Tab cicla entre os modos de permissão (Manual, Accept edits, Plan e Auto)

No modo Plan, o Claude explora o projeto e propõe um plano sem editar os arquivos de código

É exatamente o que você quer no começo de uma migração: alguém olhando o mapa, não alguém já renomeando arquivo 🙂

Passo a passo da migração com o Claude Code

A ordem abaixo tem uma lógica: primeiro você deixa JS e TS conviverem, depois liga a checagem, e só no fim aperta o parafuso

Apertar o parafuso antes é o que gera aquela tela de 400 erros que faz todo mundo desistir

1. Plan mode: mapear a ordem dos arquivos antes de tocar em qualquer um:

Shift+Tab até cair no Plan e pede o mapa

Algo assim:

Mapeie os módulos deste projeto por dependência.
Quero uma ordem de migração JS -> TS começando pelas folhas
(arquivos que ninguém importa de volta) e terminando nos entrypoints.
Não edite nada, só me devolva a lista em lotes de 3 a 5 arquivos.

O agente lê e propõe, sem editar código

O erro comum deste passo: aceitar o plano sem olhar

A ordem folhas primeiro é uma convenção prática, não uma regra do compilador, então trate como sugestão e confira se faz sentido no SEU projeto

2. Gerar o tsconfig e ligar o allowJs:

npx tsc --init

Depois liga o allowJs, que é a opção que permite arquivos JavaScript serem importados dentro do projeto, e não apenas .ts e .tsx

{
  "compilerOptions": {
    "allowJs": true,
    "noEmit": true
  }
}

Esse é o segredo da migração em fatias: com allowJs ligado, .js e .ts moram juntos e o projeto continua rodando enquanto você converte

O erro comum deste passo: esquecer o allowJs e ver o build reclamar de import de arquivo que sempre existiu

3. Ligar o checkJs pra ver os erros ANTES de converter:

Essa é a parte que quase todo mundo pula

O checkJs funciona em conjunto com o allowJs e faz o compilador reportar erros nos arquivos JavaScript

Ele equivale a colocar // @ts-check no topo de todos os .js incluídos no projeto

{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,
    "noEmit": true
  }
}

Se ligar em tudo de uma vez virar um mar vermelho, dá pra ir de arquivo em arquivo: basta o comentário // @ts-check na primeira linha do .js

E onde o arquivo ainda vai ficar .js por um tempo, JSDoc resolve, porque comentário JSDoc pode ser usado pra adicionar informação de tipo a código JavaScript, sem converter o arquivo:

// @ts-check

/**
 * @param {string} nome
 * @param {number} total
 * @returns {string}
 */
function resumo(nome, total) {
  return `${nome}: ${total}`
}

Muito massa isso, porque você já colhe tipo antes de renomear um arquivo sequer

O erro comum deste passo: pedir pro agente "consertar todos os erros do checkJs" de uma vez

Aí ele sai editando meio mundo de arquivo e você perde a régua de review

4. Renomear o PRIMEIRO arquivo .js para .ts:

O primeiro passo da migração descrito no manual oficial do TypeScript é exatamente esse: renomear um dos seus arquivos .js para .ts

Um arquivo

Depois roda o juiz:

npx tsc --noEmit

Como o noEmit não gera arquivo de saída, essa rodada é só checagem de tipo, rápida e sem sujeira no disco

O erro comum deste passo: deixar o agente renomear 20 arquivos porque "tava indo bem"

Quando quebra, você não sabe qual dos 20 quebrou

5. Repetir por lotes pequenos, com o projeto rodando entre eles:

Aqui é o loop da migração inteira:

  1. renomeia o lote (3 a 5 arquivos)
  2. roda npx tsc --noEmit
  3. roda o projeto/os testes
  4. commita
  5. próximo lote

Rodar o projeto entre os lotes não é frescura: type-check verde e app quebrado são coisas diferentes, principalmente quando o rename mexeu em extensão de import

Dá pra automatizar parte disso com hook

Hooks do Claude Code são configurados no settings.json (por exemplo ~/.claude/settings.json) dentro de um objeto hooks, e um hook PostToolUse com matcher Edit|Write dispara um comando depois de cada edição de arquivo:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "npx tsc --noEmit" }
        ]
      }
    ]
  }
}

Tome cuidado com a expectativa aqui: o PostToolUse não consegue bloquear a operação, porque a ferramenta já rodou

O que ele faz é devolver a saída de stderr pro Claude ver, ou seja, o agente fica sabendo que quebrou e pode corrigir

É feedback, não cadeado

6. Apertar o noImplicitAny:

Com a base já em .ts, entra a caça aos any invisíveis

O noImplicitAny faz o TypeScript emitir erro sempre que ele teria inferido o tipo any silenciosamente

{
  "compilerOptions": {
    "noImplicitAny": true
  }
}

O erro comum deste passo: pedir pro agente "resolver os erros" e ele anotar tudo como any explícito

Parabéns, o erro sumiu e a tipagem também haha

Revisa parâmetro por parâmetro

7. Ligar o strict:

A flag strict é um guarda-chuva: ela liga várias opções de checagem estrita de uma vez, a família strict, com coisas como strictNullChecks e strictFunctionTypes

{
  "compilerOptions": {
    "strict": true
  }
}

Por isso ela vem no fim, e não no início

Ligar strict no dia 1 de uma base JS grande é o caminho mais curto pra abandonar a migração

8. Higiene das supressões:

Toda migração acumula supressão pelo caminho, e tudo bem

O problema é a supressão que fica lá pra sempre

Troca // @ts-ignore por // @ts-expect-error

Porque o // @ts-ignore não faz nada se a linha seguinte não tiver erro (ele fica calado), enquanto o // @ts-expect-error reporta Unused @ts-expect-error directive quando a supressão deixa de ser necessária

Traduzindo: um deles te avisa quando a dívida foi paga, o outro te deixa carregando ela pra sempre

Onde a conversão automática esconde erro (e o que revisar à mão)

A conversão automática acerta MUITO na parte mecânica: renomear, ajustar import, criar assinatura óbvia, mover module.exports pra export

O que ela não faz é entender a sua regra de negócio

E é aí que mora o problema, porque o resultado compila e você acha que acabou

Sintoma: o arquivo virou .ts, o tsc --noEmit passou verde, mas nada tá realmente tipado

Causa: o noImplicitAny ainda tá desligado, então tudo que o compilador não conseguiu inferir virou any no silêncio

Solução: ligar noImplicitAny e revisar um a um, no lugar de aceitar o verde

Sintoma: um erro que existia ontem sumiu e ninguém consertou nada

Causa: tem um // @ts-ignore novo na linha de cima

Solução: trocar por // @ts-expect-error, que pelo menos reclama quando a supressão virou desnecessária, e revisar no diff toda supressão que apareceu no lote

Sintoma: o tipo passa no compilador mas não descreve o dado real

Causa: inferência a partir de um uso feliz, do tipo { id: number, nome: string } porque foi assim que o objeto apareceu naquele arquivo, ignorando que a API às vezes devolve nome vazio ou nulo

Solução: revisar à mão a assinatura pública (o que sai do módulo e é usado por outros) e os valores que podem ser nulos, com o strictNullChecks da família strict ajudando a puxar a orelha

Como prevenir tudo isso? Lote pequeno, tsc --noEmit entre as etapas e disciplina de contexto

Se a sessão ficar longa (e ela FICA, migração é conversa comprida), lembra que o /clear zera o contexto da conversa (a sessão anterior continua em disco e pode ser retomada pelo ID) e o /compact resume o histórico pra liberar contexto, aceitando instrução de foco

Sessão entupida é onde o agente começa a esquecer a regra do lote…

Migração guiada pelo agente ou ferramenta automática: quando cada uma faz sentido

Não é briga, são cenários diferentes

Projeto pequeno, ou com regra de negócio delicada, pede conversão em lotes conduzida no Claude Code

E tem um recurso que cai bem aqui: cada subagente roda na própria janela de contexto, com system prompt próprio e acesso de ferramentas próprio

As chamadas intermediárias ficam dentro do subagente e só a mensagem final volta pra conversa principal, e vários podem rodar em paralelo

Ou seja: um subagente por lote, a conversa principal recebendo só o resultado, e o seu contexto principal não vira um depósito de log

Base grande e repetitiva é outra conversa

Aí faz sentido olhar pro ts-migrate, ferramenta open source mantida no repositório airbnb/ts-migrate, que pega um projeto JavaScript ou parcialmente TypeScript e devolve um projeto TypeScript que compila

Repara na frase: que compila

Situação Caminho que combina O que você ganha O que continua com você
Projeto pequeno ou regra de negócio delicada Lotes conduzidos no Claude Code, subagente por lote Revisão a cada etapa, projeto rodando entre os lotes Ler o diff e revisar os tipos gerados
Base grande e repetitiva ts-migrate pra passada mecânica Projeto que compila, ponto de partida uniforme Rever os tipos depois, um módulo por vez

E fica com isso na cabeça: compilar não é o mesmo que estar tipado

Uma passada automática te dá o esqueleto

O tipo que descreve o dado de verdade é revisão humana, com ou sem agente do lado

Próximo passo

A régua da migração cabe em uma linha: um lote por vez, projeto rodando entre as etapas, compilador como juiz

O agente é o executor rápido, não o revisor final

O próximo passo concreto é ridículo de simples: pega UM arquivo .js hoje, renomeia pra .ts e roda

npx tsc --noEmit

Se passar, commita e escolhe o próximo

Se não passar, você acabou de descobrir a primeira dívida de tipo do projeto, e descobrir isso em um arquivo é bem melhor que descobrir em cinquenta 😀

Uma nota de contexto sobre versão, porque isso entra no plano: o TypeScript 7.0 estável foi anunciado pela Microsoft no blog oficial em 08/07/2026

Ele é um port nativo do compilador, apresentado como cerca de 10x mais rápido na checagem de tipos, e foi feito pra ser compatível com o comportamento de checagem e de linha de comando do 6.0, adotando os padrões novos do 6.0 e transformando em erro duro flags e construções que o 6.0 depreciou

Traduzindo pro seu caso: revisar as flags do tsconfig.json faz parte do plano de migração, não é detalhe pra depois

até o próximo post!

Perguntas frequentes

Preciso converter todos os arquivos .js de uma vez para migrar JavaScript para TypeScript?

Não, e tentar isso é o que mais quebra projeto no meio do caminho. Com allowJs ligado, arquivos .js e .ts convivem no mesmo projeto, então dá pra renomear em lotes pequenos (3 a 5 arquivos) e rodar o compilador entre um lote e outro.

Qual a diferença entre allowJs e checkJs no tsconfig?

O allowJs só permite que arquivos .js sejam importados dentro de um projeto com .ts e .tsx, sem checar tipo neles. Já o checkJs funciona junto com o allowJs e faz o compilador reportar erro nesses .js, o que equivale a colocar // @ts-check no topo de cada um deles.

Dá pra ter checagem de tipo em um arquivo JavaScript sem renomear ele para .ts?

Dá sim, de duas formas. Colocando // @ts-check na primeira linha do arquivo você liga a checagem naquele .js isolado, e usando comentário JSDoc você adiciona informação de tipo ao código sem precisar converter o arquivo pra .ts.

Quando faz sentido ligar noImplicitAny ou o modo strict na migração?

Só depois que o projeto já compila com allowJs e checkJs ligados, nunca no primeiro dia. O noImplicitAny emite erro sempre que o tipo seria inferido como any silenciosamente, e o strict é o guarda-chuva que liga esse e outros checks (como strictNullChecks e strictFunctionTypes) de uma vez.

Qual a diferença entre @ts-ignore e @ts-expect-error para suprimir erro de tipo?

O // @ts-ignore não faz nada se a linha seguinte não tiver erro, então ele pode ficar esquecido no código pra sempre sem avisar ninguém. Já o // @ts-expect-error reporta ‘Unused @ts-expect-error directive’ quando a supressão deixou de ser necessária, o que ajuda a limpar gambiarra depois que o tipo foi corrigido de verdade.

Existe alguma ferramenta pra automatizar a conversão de JavaScript para TypeScript?

Sim, o ts-migrate é a ferramenta open source citada no post, que pega um projeto JavaScript ou parcialmente TypeScript e devolve um projeto TypeScript que compila. Mesmo assim vale revisar o resultado com o Claude Code em vez de confiar que a conversão automática já saiu pronta.



Subscribe
Notify of
guest

0 Comentários
Oldest
Newest Most Voted

Formações

Formação Vibe Coding

Formação Vibe Coding

Do Prompt ao Produto: Crie Software Real com IA

  • 474 aulas
  • 20 projetos
  • 39h 27min

Blog | Mais populares