Migre para o TypeScript 7 sem quebrar seu monorepo

Por Diogo Hudson Dias
Senior engineer in a São Paulo office examining a tsconfig file and CI dashboard on dual monitors during the evening.

Atualizações maiores do TypeScript não quebram seu app em produção. Elas quebram seu CI, seu editor e sua agenda. Com o TypeScript 7 não será diferente. O verificador de tipos fica mais rígido, velhas brechas se fecham e seu monorepo fica todo vermelho porque o narrowing de uniões ficou mais esperto em um canto e seu plugin de decorators atrasou uma semana em outro. Se você tratar isso como um bump de dependência de sexta‑feira, vai perder um sprint.

Isso não é um manifesto contra upgrades. É um playbook para migrar para o TypeScript 7 sem paralisar a entrega. O objetivo: continuar enviando features enquanto vira o compilador. Isso significa dois pipelines (build vs. tipos), flags em estágios, uma baseline que você faz tender a zero e uma equipe de choque pequena e responsável. Se você lidera engenharia em uma startup nos EUA rodando um patrimônio de 300K–2M LOC em TypeScript, continue lendo.

Por que este upgrade importa para um CTO

  • A semântica do compilador muda em majors. Mesmo quando o output em runtime é idêntico, uma mudança na análise de fluxo de controle ou na tipagem de JSX pode invalidar milhares de linhas. Seu CI vermelho não é um bug; é a nova realidade.
  • Seu ecossistema se move com você. Frameworks, linters e pacotes de tipos condicionam suporte por major. Se seu produto depende de Next.js, Vite ou de um design system que fixa tooling, você acabará forçado a se mover.
  • O custo é real, mas limitado. Na prática, vemos 0,5–1,5 semana de FTE por 100K LOC para migrar uma base saudável com pipeline maduro. Em um monorepo de 1M LOC, isso dá 1–3 semanas focadas para um pequeno pod de plataforma.

O que costuma quebrar times (independentemente dos detalhes da versão)

  • Deriva na resolução de módulos. Pacotes ESM/CJS duplos, maps de "exports" e modos diferentes de moduleResolution desestabilizam aliases de path e deep imports. Seu config do Jest/Vitest geralmente fica atrás do seu config de build.
  • Flags padrão mais rígidas ou inferência mais estreita. Um checker mais esperto revela padrões inseguros dos quais você dependia. Geralmente 70–80% dos erros se concentram em alguns poucos antipadrões.
  • Decorators e metadata. Se você usa ORMs ou frameworks com decorators, está refém do ritmo de releases deles. Um pequeno atraso pode bloquear um upgrade maior do compilador.
  • Acoplamento entre build e tipos. Babel/esbuild/swc transpila sem problemas enquanto o tsc falha, ou vice‑versa. Times que amarraram o emit à verificação de tipos precisam escolher um para destravar o outro.

A meta‑estratégia: dois pipelines, uma baseline

Pare de misturar compilação com verificação de tipos. Seu pipeline de runtime não deve depender do humor do checker hoje.

  1. Separe build e tipos. Use esbuild ou swc para transpilar/empacotar. Rode tsc --noEmit para verificação de tipos. Mantenha jobs separados no CI com cache e responsáveis independentes.
  2. Adicione um estágio “dueto” de TypeScript. Por algumas semanas, rode TypeScript 6 (atual) e TypeScript 7 (upgrade) concurrentemente no CI. TS6 continua como gate. TS7 reporta para um arquivo de baseline que deve cair diariamente.
  3. Estabeleça uma baseline. Primeiro run do TS7: capture todos os erros em uma baseline. Não isente tudo globalmente. Crie supressões direcionadas, revisadas, no código e com data clara de expiração.

Higiene pré‑migração (um sprint de trabalho chato que se paga)

1) Faça inventário da sua superfície

  • Tamanho e forma: Conte LOC por pacote. Identifique os 20 principais pacotes por tempo de compilação e densidade de erros quando você fizer um build de teste. Você precisa de um mapa antes de escolher as batalhas.
  • Buracos de tipo: Meça a taxa de uso de any. Se mais de 10–15% dos arquivos dependem de any implícito ou de as any agressivo, ajuste expectativas: a migração vai deixar muita coisa em vermelho.
  • Pé do test runner: Quantos configs de Jest? Algum ts-jest legado? Se sim, planeje migrar para ts-jest com transpile isolado ou mudar para Vitest com esbuild/swc para evitar acoplamento com tipos.

2) Desenrede a resolução de módulos

  • Padronize na semântica NodeNext. Mistura de pacotes ESM/CJS é um fato. Alinhe tsconfig, runtime do Node, bundler e test runner na mesma história de módulos. Não entregue em produção um grafo de módulos diferente do que você testa localmente.
  • Mate o hábito de deep imports. Proíba imports como lib/something/internal entre pacotes. Use entry points públicos. Adicione uma regra de lint para flagrar deep imports.
  • Mantenha os aliases de path honestos. Se tsconfig.paths mapeia @app/foo para src/foo, seu bundler e test runner devem resolver da mesma forma. Deriva aqui cria “ação fantasma à distância” nos upgrades.

3) Adote project references

Se você roda um monorepo e não usa project references do TypeScript, está deixando 20–40% de tempo de compilação na mesa e tem menos alavancas durante upgrades. Builds compostos dão limites por pacote e cacheabilidade que tornam os ciclos vermelho/verde suportáveis.

  • Deixe os pacotes compostos. Adicione composite: true e limites. Gere .d.ts para pacotes folha que exportam tipos.
  • Use tsc -b localmente e no CI. O modo de build captura ciclos e controla invalidação. Em um repo de 1M LOC, vemos rotineiramente o cold build cair de 15–20 minutos para 6–10 com references e caching.

4) Estagie flags mais rígidas antes do salto

Você reduz surpresas ativando checagens mais estritas no compilador atual. Escolha 2–3 flags que seu código tolere e faça ramp‑up com exceções controladas:

  • noUncheckedIndexedAccess para expor acessos inseguros a array/objeto.
  • exactOptionalPropertyTypes para parar de confundir “ausente” com “undefined”.
  • useUnknownInCatchVariables para acabar com o any preguiçoso em tratadores de erro.

Ative em alguns pacotes com uma cota clara de correções por equipe por semana. O payoff é um raio de impacto menor quando você virar para TS7.

A migração, passo a passo

Fase 0 (1–2 dias): Congelar e bifurcar

  • Congele o mundo. Congele sua ferramenta: versão do Node, gerenciador de pacotes, bundler, test runner e pacotes de tipos. Atualizar o compilador enquanto também troca Node ou Jest é como times criam regressões de uma semana.
  • Faça um fork do compilador. Adicione TypeScript 7 como uma dev dependency separada em um job paralelo no CI. Mantenha TypeScript 6 como o checker que faz gate no main.
  • Crie a baseline de erros do TS7. O primeiro run completo de tsc --noEmit com TS7 gera sua “dívida”. Armazene. Publique diariamente. Seu pod de plataforma é dono de levar a zero.

Fase 1 (3–5 dias): Abater os 80%

  • Agrupe os erros. Em um repo de 500K–1M LOC, espere 1.000–5.000 erros iniciais. 70–80% são padrões repetitivos. Exemplos que sempre vemos: falta de guardas satisfies para literais de objeto, tipos condicionais frágeis expostos por narrowing mais estrito e vazamento de alias de path para runtime.
  • Automatize os consertos tediosos. Use ts-morph ou codemods para aplicar o mesmo refactor local em dezenas de pacotes. Adicione regras de lint para que o antipadrão não volte a crescer.
  • Publique o burn‑down. Trate como redução de incidentes: gráfico diário para a organização. Você quer uma linha reta rumo a zero até o fim da primeira semana.

Fase 2 (2–4 dias): Os 20% teimosos

  • Decorators e atraso do ecossistema. Se você depende de decorator ou plugins de reflexão de um framework, coordene com esse time ou fornecedor. Fixe em uma versão estável. Se o suporte estiver pendente, isole esses pacotes atrás de um limite temporário e isente só ali.
  • Superfícies de API pública. Bibliotecas que publicam .d.ts precisam de atenção especial. Rode o API Extractor para verificar se a API pública está estável após a troca do compilador.
  • Paridade do test runner. Garanta que Jest/Vitest resolvem ESM/CJS da mesma forma que seu bundler e o Node. Descompassos aqui causam “funciona no dev, falha no CI”, que parecem regressões do TypeScript mas são bugs de módulo.

Fase 3 (1 dia): Vire o gate

  • Promova TS7 a gate. Quando a baseline chegar a zero e a branch principal estiver verde por 48 horas com o job em dueto, torne o TS7 o checker que faz gate. Mantenha o job de TS6 por uma semana como canário e depois remova.
  • Descongele. Desfixe o ecossistema com cuidado. Atualize uma classe de ferramenta por dia (linters, test runners, depois bundlers) para evitar explosões combinatórias.

Guardrails de performance e confiabilidade

  • Faça cache de tudo. Faça cache remoto do output de esbuild/swc e dos artefatos de tsc -b. Em um repo de 1M LOC, o tempo de CI cai 30–50% com chaves de cache confiáveis.
  • Mantenha skipLibCheck ligado no CI a menos que você publique bibliotecas. Nesse caso, adicione um job noturno separado de “libcheck” para pegar deriva de pacotes de tipos sem taxar todo PR.
  • Meça o delta. Acompanhe tempo de build e tamanho de bundle antes/depois. Em uma base saudável, espere o wall‑time de tsc --noEmit dentro de ±10% e nenhuma mudança significativa em runtime. Se vir regressões maiores, geralmente é uma nova explosão de recursão em nível de tipo em um único pacote — ache e simplifique.

Pessoas e modelo de custo

Você não precisa de um enxame. Precisa de um pod pequeno que faça mudanças pouco glamorosas rápido e coordene com calma.

  • Time: 2–4 engenheiros sêniores de plataforma/tooling com autoridade para tocar todos os pacotes. Adicione um representante do seu maior time de produto para contexto.
  • Tempo: 1–3 semanas para 1M LOC. Monorepo saudável com project references e stack moderno de teste/build tende para uma semana. Se você não tem references e ainda transpila via tsc, reserve três.
  • Overlap importa. Se você usa um pod nearshore no Brasil, terá 6–8 horas de sobreposição com os EUA para o burn‑down diário e acerto de dependências, e custo 20–30% menor para a monotonia de codemods e diff de configuração.

O que vai quebrar (e como desarmar)

  • Jest com ts-jest em repositórios ESM. Se você ainda compila tipos via Jest, migre para ESM nativo com Babel/esbuild para transform e deixe tipos para tsc --noEmit. Ou adote Vitest onde der.
  • Aliases de path em tempo de execução. Imports como @app/foo funcionam no editor mas explodem no Node. Resolva isso no bundler/test e gere imports relativos reais no código emitido. Uma checagem prévia simples de “sem aliases não resolvidos” economiza horas.
  • Barrel files e dependências circulares. O checker fica melhor em detectar ciclos sutis. Rode o dependency-cruiser e quebre ciclos explicitamente antes da virada.
  • Vazamento de tipos ambientes vindos de testes. Declarações globais sangram para o código‑fonte em monorepos com globs relaxados. Delimite seu campo types no tsconfig e separe tipos de teste dos de produção.

Um padrão real de resultados (anonimizado)

Em uma fintech em crescimento com ~700K LOC de TypeScript em 86 pacotes, fizemos um upgrade de major de TS em duas semanas sem congelar features:

  • Semana 0: Adicionamos project references a 18 pacotes atrasados; tiramos testes de ts‑node; padronizamos resolução de módulos.
  • Semana 1: Checker duplo no CI (antigo como gate, novo como baseline). 2.840 erros iniciais no TS7; 2.250 corrigidos com três codemods e dois lints. Tempo de CI caiu de 28 para 17 minutos após references e cache remoto.
  • Semana 2: Pacote de ORM pesado em decorators bloqueado por 3 dias aguardando patch do fornecedor; isolado e isentado localmente. Baseline a zero; promovemos TS7 a gate. Zero incidentes em produção. Sentimento dos devs: menos erros fantasma no IntelliSense.

Quando você não deveria migrar ainda

  • Seu framework core não está pronto. Se seu app depende de um framework que não declarou suporte, espere. Não faça hard‑fork do seu plugin de decorators a menos que esteja disposto a assumi‑lo.
  • Você também está trocando Node, bundler ou test runner. Separe as mudanças. Um eixo por vez. A matemática da confiabilidade não joga a seu favor do contrário.
  • Sua dívida de tipos é catastrófica. Se any implícito está por toda parte e seus testes dependem de ts‑node, faça antes um sprint de hardening. Você poupa dois no upgrade.

Governança: preserve os ganhos

  • Adicione um ritual trimestral de upgrade do compilador. Coloque no roadmap de plataforma. Passos pequenos vencem saltos de anos.
  • Mantenha o job em “dueto”. Você não precisa de dois majors para sempre, mas repita o padrão de dueto em cada minor/major. É um seguro barato.
  • Acompanhe a saúde de tipos como um KPI. Taxa de any, número de supressões e densidade de erros por pacote são indicadores antecedentes da dor no próximo upgrade.

Se você quiser ajuda

Nossos pods nearshore no Brasil fazem esse trabalho o ano todo: desenredar resolução de módulos, espalhar project references em monorepos, aplicar codemods para eliminar padrões frágeis e manter o CI verde enquanto você entrega. Você ganha sobreposição com os EUA (6–8 horas), checklists testados e um time que já quebrou (e consertou) essas coisas antes.

Pontos‑chave

  • Separe build de tipos; rode esbuild/swc para emitir e tsc --noEmit para checar.
  • Rode um CI com checker duplo (atual como gate, TS7 como baseline) por 1–2 semanas.
  • Adote project references e cache remoto para domar tempos de compilação.
  • Padronize uma história de módulos (NodeNext) em runtime, bundler e testes.
  • Corrija os 70–80% de erros repetitivos com codemods; isole os 20% teimosos.
  • Reserve 0,5–1,5 semana de FTE por 100K LOC; use um pod pequeno de plataforma, não uma manada.
  • Não faça upgrade junto com mudanças grandes de Node/bundler/test; um eixo por vez.
  • Trate a saúde de tipos como métrica de produto para o próximo upgrade ser entediante.

Ready to scale your engineering team?

Tell us about your project and we'll get back to you within 24 hours.

Start a conversation