El editor encuentra el import, pero el programa falla cuando Node carga el JavaScript. Ocurre lo contrario cuando TypeScript pasa, pero el bundler o los archivos de declaración esperan otra cosa. La elección entre NodeNext y Bundler empieza con una pregunta: ¿quién cargará el resultado?

Usa NodeNext cuando Node.js cargará el JavaScript directamente. Usa Bundler cuando un bundler resolverá los imports y producirá el artefacto final. Si el problema también incluye varios paquetes y el orden del build, Project References en TypeScript es un tema vecino, no un sustituto de esta decisión.

Diagrama que compara NodeNext y Bundler a partir de un import de TypeScript y muestra quién carga el JavaScript generado.

Respuesta corta

  • Para un servicio, CLI o biblioteca de Node que debe funcionar sin bundling, empieza con module: "NodeNext" y moduleResolution: "NodeNext".
  • Para una aplicación cuyo bundler produce el JavaScript final, usa el modo de módulo compatible con ese bundler y moduleResolution: "Bundler".
  • Con NodeNext, escribe extensiones .js en los imports ESM relativos, incluso dentro de archivos .ts.
  • No ejecutes directamente en Node un output que todavía depende de un bundler para resolver imports.
  • Verifica module y moduleResolution junto con package.json, exports y el comando que realmente se ejecuta en producción.

¿Qué decide moduleResolution?

moduleResolution define cómo TypeScript encuentra el módulo indicado por un import. No es solo una preferencia de estilo. El modo elegido intenta modelar el loader que verá el JavaScript después de la compilación. La referencia oficial de moduleResolution describe node16 y nodenext para el comportamiento moderno de Node y bundler para los bundlers, consultada el 19 de octubre de 2026.

module y moduleResolution trabajan juntos, pero responden a preguntas distintas. module indica cómo TypeScript debe tratar o emitir los módulos. moduleResolution indica qué reglas debe usar al encontrar imports, exports y entradas de paquetes. Con NodeNext, TypeScript combina estas decisiones con el formato indicado por las extensiones y por el campo type del package.json más cercano.

Situación real Par inicial Qué debe ser cierto
Node ejecuta los archivos emitidos sin bundler module: "NodeNext" + moduleResolution: "NodeNext" El output respeta las reglas ESM o CommonJS de Node
Vite, esbuild, Rollup u otro bundler produce el artefacto final module: "ESNext" o "Preserve" + moduleResolution: "Bundler" El bundler resuelve los imports y emite el formato que se ejecutará
Una biblioteca publica declaraciones y JavaScript para consumidores de Node NodeNext suele ser un punto de partida más seguro Los imports preservados en .d.ts también funcionan para los consumidores

La tabla es un punto de partida, no un preset universal. package.json, el formato publicado y el pipeline de build siguen formando parte del contrato.

¿Cuándo conviene NodeNext?

Elige NodeNext cuando Node cargará el output: un servicio, CLI, worker o biblioteca que entrega archivos que el consumidor puede ejecutar sin bundling. La documentación de TypeScript sobre la teoría de módulos explica que este modo comprueba las reglas de resolución de Node, incluida la diferencia entre ESM y CommonJS.

En un proyecto ESM mínimo, el par debe coincidir con package.json:

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

En el archivo TypeScript, escribe la extensión del archivo que existirá después de la compilación:

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

console.log(answer);

TypeScript puede relacionar ./utils.js con el archivo fuente utils.ts durante la comprobación, mientras que Node encontrará utils.js en el directorio de salida. Un import como ./utils puede parecer correcto en el editor y aun así fallar en el ESM de Node porque el loader no adivina la extensión.

¿Cuándo conviene Bundler?

Usa Bundler cuando el import no se ejecutará tal cual en el output de tsc. Un bundler puede aceptar imports relativos sin extensión, construir un grafo, eliminar módulos y producir su propio artefacto. La referencia de TypeScript indica que bundler admite imports y exports de package.json, pero no exige la extensión relativa que requieren los modos modernos de Node.

Una configuración mínima para ese caso es:

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

TypeScript puede aceptar este import en ese modo:

import { answer } from "./utils";

export function readAnswer() {
  return answer;
}

Eso no demuestra que dist/main.js pueda abrirse con Node. Si el bundler nunca se ejecuta, el import sigue sin extensión y el runtime puede devolver ERR_MODULE_NOT_FOUND. Bundler describe la frontera que debe resolver el grafo. No convierte el output de tsc en un bundle.

¿Qué cambia en una biblioteca con archivos .d.ts?

Las bibliotecas tienen una trampa adicional. Aunque el JavaScript principal se empaquete, TypeScript puede emitir varios archivos .d.ts. Esos archivos conservan imports que un consumidor de Node debe resolver. La guía de TypeScript sobre cómo elegir opciones de compilación advierte que bundler puede aceptar un import que funciona en un bundle y dejar una declaración incompatible con Node cuando las declaraciones tampoco se empaquetan.

Por eso, si una biblioteca publica declaraciones individuales y espera consumidores que ejecutan en Node, NodeNext suele ser una comprobación más conservadora. Si la herramienta empaqueta tanto JavaScript como declaraciones, Bundler puede ser apropiado, pero esa herramienta queda a cargo de producir y probar el paquete final.

No confundas este contrato de módulos con el contrato de un valor. El artículo sobre satisfies en TypeScript explica cómo comprobar un objeto escrito en el código sin perder la inferencia. El JSON recibido por la red necesita validación en runtime, como explica la guía sobre validar JSON externo antes de usarlo en TypeScript.

¿Cómo verificar la decisión con una fixture pequeña?

No empieces cambiando el tsconfig de todo un monorepo. Reproduce primero la frontera con dos proyectos pequeños. Esta fixture se compiló el 19 de octubre de 2026 con TypeScript 5.9.3. El proyecto NodeNext importa ./utils.js desde src/main.ts; el proyecto Bundler usa ./utils para el mismo import.

Para el proyecto NodeNext:

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

El comando terminó sin diagnósticos e imprimió 42. La compilación y la ejecución comprueban dos partes del contrato: el compilador acepta el import escrito para el output y Node encuentra el archivo emitido.

Para el proyecto Bundler:

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

TypeScript también terminó sin diagnósticos. El output conservó import { answer } from "./utils". Al ejecutar ese output directamente con Node, la fixture falló con ERR_MODULE_NOT_FOUND, porque ningún bundler había resuelto el import. No es un fallo del modo Bundler. Demuestra que el archivo todavía no era el artefacto final para ese runtime.

Cuando lleves la prueba al repositorio real, verifica cuatro cosas:

  1. el comando que compila o empaqueta en CI;
  2. el archivo exacto que inicia el proceso de producción;
  3. los campos type, exports e imports relevantes de package.json;
  4. el formato y los imports de los .d.ts si el proyecto publica una biblioteca.

¿Qué errores de configuración aparecen con más frecuencia?

El primero es elegir Bundler para un servicio que se ejecuta directamente en Node porque los imports son más fáciles de escribir. El editor y tsc siguen en verde, pero el archivo emitido no tiene un resolver para el comportamiento que el modo presupone.

El segundo es elegir NodeNext sin aceptar sus reglas. El proyecto mantiene imports relativos sin extensión, mezcla module: "CommonJS" con moduleResolution: "NodeNext" o cambia type: "module" sin comprobar cómo se interpretarán los archivos. La solución no es añadir una flag al azar. Alinea el módulo, la resolución, package.json y el runtime.

El tercero es validar solo el workspace local. Un paquete puede funcionar allí porque un alias, bundler o runner resuelve algo que el consumidor final no tendrá. Construye el paquete, instálalo en una fixture limpia y ejecuta el mismo camino que ejecutará su usuario.

Si el repositorio también tiene varios paquetes, separa esta pregunta del orden de compilación. Project References en TypeScript organiza el grafo de proyectos y tsc -b; moduleResolution decide cómo encuentra módulos cada proyecto. Uno no sustituye al otro.

Checklist antes de cambiar el modo

  • ¿Quién carga el JavaScript final: Node, un navegador, un bundler o más de uno?
  • ¿module y moduleResolution forman un par que coincide con ese loader?
  • ¿Los imports ESM relativos usan la extensión que existirá en el output?
  • ¿package.json declara el formato que Node debe interpretar?
  • ¿Probaste exports e imports desde el paquete publicado y no solo desde el workspace?
  • ¿CI ejecuta tsc, un bundler o los dos?
  • ¿Una fixture limpia puede compilar y ejecutar el artefacto real?
  • ¿Los .d.ts siguen resolviendo en el entorno de quien consume la biblioteca?

El mejor modo no es el más permisivo. Es el modo que hace visible el fallo probable en el mismo lugar donde se rompería después. Si Node carga el archivo, haz que TypeScript compruebe Node. Si un bundler produce el artefacto, haz que TypeScript modele el bundler y prueba el bundle. El resto de la decisión queda visible en el pipeline.

Cómo se produjo este artículo

Samuel Fajreldines es responsable de este artículo. La investigación comparó las referencias actuales de TypeScript sobre module, moduleResolution, teoría de módulos y selección de opciones de compilación con la documentación de Node.js sobre TypeScript. También se revisaron resultados actuales en portugués, inglés y español, además de discusiones públicas usadas solo para vocabulario y objeciones.

La fixture local usó TypeScript 5.9.3. El proyecto NodeNext compiló y se ejecutó con salida 42; el proyecto Bundler compiló, conservó el import sin extensión y falló al ejecutarse directamente con Node. Esto demuestra una frontera pequeña, no un benchmark de Vite, esbuild, Rollup, frameworks o bibliotecas publicadas.

Usé asistencia de IA para descubrir fuentes, organizar, redactar, generar la imagen, localizar y revisar la consistencia. No sustituyó la verificación del compilador ni del runtime, no midió producción y no añadió experiencia de proyecto que no se haya realizado.

Fuentes consultadas