O editor encontra o import, mas o programa falha quando o JavaScript chega ao Node. Ou o inverso: a configuração passa no TypeScript, mas o bundler e os arquivos de declaração esperam outra coisa. A escolha entre NodeNext e Bundler começa por uma pergunta simples: quem vai carregar o resultado?

Use NodeNext quando o JavaScript será carregado diretamente pelas regras do Node.js. Use Bundler quando um empacotador vai resolver os imports e produzir o artefato final. Se o problema também envolve vários pacotes e a ordem do build, Project References no TypeScript é um assunto vizinho, não um substituto para esta escolha.

Diagrama compara NodeNext e Bundler a partir de um import TypeScript e mostra quem carrega o JavaScript produzido.

Resposta curta

  • Para um serviço, CLI ou biblioteca que precisa funcionar sem bundling no Node, comece com module: "NodeNext" e moduleResolution: "NodeNext".
  • Para uma aplicação cujo bundler produz o JavaScript final, use o modo de módulo compatível com esse bundler e moduleResolution: "Bundler".
  • No NodeNext, escreva extensões .js nos imports relativos ESM, mesmo no arquivo .ts.
  • Não execute diretamente no Node um output que ainda depende de um bundler para resolver imports.
  • Verifique o par module e moduleResolution junto com package.json, exports e o comando que realmente roda em produção.

O que moduleResolution decide?

moduleResolution define como o TypeScript encontra o módulo indicado por um import. Ele não é apenas uma preferência de estilo. O modo escolhido tenta modelar o carregador que verá o JavaScript depois da compilação. A referência oficial de moduleResolution descreve node16 e nodenext para o comportamento moderno do Node e bundler para os empacotadores, consultada em 19 de outubro de 2026.

module e moduleResolution trabalham juntos, mas respondem a perguntas diferentes. module diz como o TypeScript deve tratar ou emitir módulos. moduleResolution diz quais regras usar para encontrar imports, exports e entradas de pacotes. Em NodeNext, o TypeScript combina essas decisões com o formato indicado por extensões e pelo type do package.json.

Situação real Par inicial O que precisa ser verdade
Node executa os arquivos emitidos sem um bundler module: "NodeNext" + moduleResolution: "NodeNext" O output precisa respeitar as regras ESM ou CommonJS do Node
Vite, esbuild, Rollup ou outro bundler produz o artefato final module: "ESNext" ou "Preserve" + moduleResolution: "Bundler" O bundler precisa resolver os imports e emitir o formato que será executado
Biblioteca publica declarações e JavaScript para consumidores Node NodeNext costuma ser o ponto de partida mais seguro Os imports preservados nos .d.ts também precisam funcionar para os consumidores

Essa tabela é um ponto de partida, não um preset universal. O package.json, o formato publicado e o pipeline de build continuam fazendo parte do contrato.

Quando NodeNext é a escolha certa?

Escolha NodeNext quando o Node será o carregador do output: um serviço, uma CLI, um worker ou uma biblioteca que entrega arquivos que o consumidor pode executar sem empacotá-los. A documentação do TypeScript sobre a teoria dos módulos explica que esse modo verifica as regras de resolução do Node, incluindo o comportamento diferente entre ESM e CommonJS.

Em um projeto ESM mínimo, o par precisa combinar com o package.json:

{
  "type": "module"
}
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022",
    "rootDir": "src",
    "outDir": "dist",
    "strict": true,
    "noEmitOnError": true
  }
}

No arquivo TypeScript, escreva a extensão do arquivo que existirá depois da compilação:

import { answer } from "./utils.js";

console.log(answer);

O TypeScript consegue relacionar ./utils.js ao arquivo-fonte utils.ts durante a verificação, enquanto o Node encontrará utils.js no diretório de saída. Um import ./utils pode parecer correto no editor e ainda falhar no ESM do Node porque o carregador não adivinha essa extensão.

Quando Bundler é a escolha certa?

Use Bundler quando o import não será executado como está no output de tsc. Um bundler pode aceitar imports relativos sem extensão, construir um grafo, eliminar módulos e produzir um arquivo ou conjunto de arquivos próprios. A referência do TypeScript observa que bundler suporta imports e exports do package.json, mas não exige a extensão relativa que os modos modernos do Node exigem.

Uma configuração mínima para esse cenário é:

{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "target": "ES2022",
    "noEmit": true,
    "strict": true
  }
}

O import abaixo pode ser aceito pelo TypeScript nesse modo:

import { answer } from "./utils";

export function readAnswer() {
  return answer;
}

Isso não prova que dist/main.js possa ser aberto pelo Node. Se o bundler não for executado depois, o import continua sem extensão e o runtime pode retornar ERR_MODULE_NOT_FOUND. Bundler descreve a fronteira que deveria resolver o grafo. Não transforma o output de tsc em um bundle.

O que muda para uma biblioteca com .d.ts?

Bibliotecas têm uma armadilha adicional. Mesmo quando o JavaScript principal será empacotado, o TypeScript pode emitir vários arquivos .d.ts. Esses arquivos preservam imports que um consumidor Node precisa resolver. A documentação do TypeScript sobre como escolher opções de compilação alerta que bundler pode aceitar um import que funciona no bundle, mas deixa uma declaração incompatível para um consumidor Node quando os arquivos de tipos não são empacotados junto.

Por isso, se a biblioteca publica declarações individuais e pretende atender consumidores que executam em Node, NodeNext costuma ser uma verificação mais conservadora. Se a ferramenta empacota JavaScript e declarações, Bundler pode fazer sentido, mas essa ferramenta passa a ter a responsabilidade de produzir e testar o pacote final.

Não confunda esse contrato de módulos com o contrato de um valor. O artigo sobre satisfies no TypeScript explica como conferir um objeto escrito no código sem perder sua inferência. Já um arquivo JSON recebido pela rede precisa de validação em runtime, como no guia sobre validar JSON externo antes de usar em TypeScript.

Como verificar a decisão com uma fixture pequena?

Não comece alterando o tsconfig de um monorepo inteiro. Reproduza primeiro a fronteira com dois projetos pequenos. Esta fixture foi compilada em 19 de outubro de 2026 com TypeScript 5.9.3. No projeto NodeNext, src/main.ts importa ./utils.js; no projeto Bundler, o mesmo import usa ./utils.

Para o projeto NodeNext:

npx -y -p typescript@5.9.3 tsc -p node/tsconfig.json --pretty false
node node/dist/main.js

O comando terminou sem diagnóstico e imprimiu 42. A compilação e a execução verificam duas partes do contrato: o compilador aceita o import escrito para o output, e o Node encontra o arquivo emitido.

Para o projeto Bundler:

npx -y -p typescript@5.9.3 tsc -p bundler/tsconfig.json --pretty false

O TypeScript também terminou sem diagnóstico. A saída preservou import { answer } from "./utils". Ao executar essa saída diretamente com Node, a fixture falhou com ERR_MODULE_NOT_FOUND, porque nenhum bundler havia resolvido o import. Esse resultado não é uma falha do modo Bundler. É a prova de que o arquivo ainda não era o artefato final para aquele runtime.

Quando levar o teste para o repositório real, verifique quatro coisas:

  1. o comando que compila ou empacota no CI;
  2. o arquivo exato que o processo de produção inicia;
  3. o type, exports e imports dos package.json relevantes;
  4. o formato e os imports dos .d.ts se o projeto publica uma biblioteca.

Quais erros de configuração aparecem com mais frequência?

O primeiro é escolher Bundler para um serviço que roda diretamente no Node porque os imports ficam mais fáceis de escrever. O editor e o tsc ficam verdes, mas o arquivo emitido não tem quem faça a resolução que o modo pressupõe.

O segundo é escolher NodeNext sem aceitar suas regras. O projeto continua usando imports relativos sem extensão, mistura module: "CommonJS" com moduleResolution: "NodeNext" ou altera type: "module" sem verificar como os arquivos serão interpretados. A correção não é adicionar uma flag aleatória. É alinhar o módulo, a resolução, o package.json e o runtime.

O terceiro é validar somente o pacote local. O pacote pode funcionar no workspace porque um bundler, um alias ou um runner resolve algo que o consumidor final não terá. Gere o pacote, instale-o em uma fixture limpa e execute o mesmo caminho que o usuário executará.

Se o repositório também tem vários pacotes, mantenha essa pergunta separada da ordem de compilação. Project References organiza o grafo de projetos e o tsc -b; moduleResolution decide como cada projeto encontra módulos. Um não substitui o outro.

Checklist antes de trocar o modo

  • Quem carrega o JavaScript final: Node, navegador, bundler ou mais de um deles?
  • module e moduleResolution formam um par coerente com esse carregador?
  • Imports relativos ESM têm a extensão que existirá no output?
  • package.json declara o formato que o Node deve interpretar?
  • exports e imports foram testados a partir do pacote publicado, não apenas do workspace?
  • O CI executa tsc, um bundler, ou os dois?
  • Uma fixture limpa consegue compilar e executar o artefato real?
  • Os .d.ts continuam resolvendo no ambiente de quem consome a biblioteca?

O melhor modo não é o mais permissivo. É o que torna o erro provável no mesmo lugar em que ele quebraria depois. Se Node carrega o arquivo, faça o TypeScript verificar Node. Se um bundler produz o artefato, faça o TypeScript modelar o bundler e teste o bundle. O restante da decisão fica visível no pipeline.

Como este artigo foi produzido

Samuel Fajreldines é o autor responsável por este artigo. A pesquisa comparou as referências atuais do TypeScript sobre module, moduleResolution, teoria de módulos e escolha de opções de compilação com a documentação do Node.js sobre TypeScript. Também foram inspecionados resultados atuais em português, inglês e espanhol e discussões públicas usadas apenas para vocabulário e objeções.

A fixture local usou TypeScript 5.9.3. O projeto NodeNext compilou e executou com saída 42; o projeto Bundler compilou, preservou o import sem extensão e falhou quando executado diretamente pelo Node. Isso demonstra uma fronteira mínima, não um benchmark de Vite, esbuild, Rollup, frameworks ou bibliotecas publicadas.

Usei assistência de IA para descoberta de fontes, organização, redação, geração da imagem, localização e revisão de consistência. A assistência não substituiu a verificação do compilador e do runtime, não mediu produção e não acrescentou experiência de projeto que não foi realizada.

Fontes consultadas