Estrategias de resolución de módulos
La resolución de módulos es el proceso mediante el cual TypeScript determina a que archivo se refiere una sentencia import. Cuando se escribe import { X } from "./módulo", el compilador necesita localizar el archivo correspondiente en disco.

TypeScript ofrece varias estrategias de resolución, cada una adaptada a un entorno de ejecución diferente. La estrategia se configura con la opción moduleResolution en tsconfig.json:
{
"compilerOptions": {
"moduleResolution": "nodenext"
}
}
Estrategia node
La estrategia node emula el mecanismo clásico de resolución de Node.js para CommonJS. Es la opción que se ha usado por defecto durante anos:
Para importaciones relativas (./ o ../):
- Busca el archivo con extensión
.ts,.tsx,.d.ts - Busca un directorio con
package.jsonque tenga campotypesomain - Busca un archivo
index.ts,index.tsxoindex.d.tsen el directorio
import { Usuario } from "./modelos/usuario"
// Busca: ./modelos/usuario.ts, ./modelos/usuario.tsx, ./modelos/usuario.d.ts
// Luego: ./modelos/usuario/package.json (campo types)
// Luego: ./modelos/usuario/index.ts
Para importaciones no relativas (sin ./):
- Busca en
node_modulesdel directorio actual - Sube un nivel y busca en el siguiente
node_modules - Continua hasta la raíz del sistema de archivos
import { format } from "date-fns"
// Busca: ./node_modules/date-fns, ../node_modules/date-fns, etc.
Estrategia nodenext
La estrategia nodenext refleja el sistema de módulos dual de Node.js moderno, que soporta tanto CommonJS como ES modules:
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext"
}
}
Las diferencias principales respecto a node:
- Extensiones obligatorias en importaciones relativas ESM
- Soporte para el campo exports de
package.json - Comportamiento diferente según el formato del módulo (ESM vs CJS)
- Soporte para subpath patterns y conditional exports
// Con nodenext, las extensiones son necesarias en archivos ESM
import { Usuario } from "./modelos/usuario.js"
// TypeScript busca ./modelos/usuario.ts (sustitución de extensión)
// Subpaths definidos en exports de package.json
import { Button } from "mi-librería/componentes"
La extensión
.jsen las importaciones se refiere al archivo JavaScript de salida, no al archivo TypeScript fuente. TypeScript realiza automáticamente la sustitución de extensión y busca el archivo.tscorrespondiente.
Detección del formato de módulo
Con nodenext, TypeScript determina si cada archivo es ESM o CJS según estas reglas:
- Archivos
.mts/.mjsson siempre ESM - Archivos
.cts/.cjsson siempre CJS - Archivos
.ts/.jsdependen del campo"type"en elpackage.jsonmás cercano"type": "module"implica ESM"type": "commonjs"o sin campotypeimplica CJS
{
"name": "mi-proyecto",
"type": "module"
}
Con esta configuración, todos los archivos .ts se tratan como ESM y las importaciones relativas requieren extensión.
Estrategia bundler
La estrategia bundler está diseñada para proyectos que usan empaquetadores como Webpack, Rollup, Vite o esbuild:
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler"
}
}
Caracteristicas de esta estrategia:
- No requiere extensiones en importaciones relativas
- Soporta el campo exports de
package.json - Permite importaciones sin extensión ni índice de directorio
- Refleja el comportamiento de los empaquetadores modernos
// Con bundler, no se necesitan extensiones
import { Usuario } from "./modelos/usuario"
// Soporta subpaths de exports
import { Icon } from "mi-ui-kit/icons"
La estrategia bundler es la más permisiva. Código que compila con bundler puede fallar en Node.js si no se usa un empaquetador. Para librerías que se publican en npm, es preferible nodenext.
Path mapping con paths y baseUrl
TypeScript permite definir alias de rutas para simplificar importaciones y evitar rutas relativas profundas:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@modelos/*": ["src/modelos/*"],
"@servicios/*": ["src/servicios/*"],
"@utils/*": ["src/utilidades/*"]
}
}
}
Con esta configuración:
// Sin alias
import { Usuario } from "../../../modelos/usuario"
import { formatearFecha } from "../../../utilidades/fecha"
// Con alias
import { Usuario } from "@modelos/usuario"
import { formatearFecha } from "@utils/fecha"
Consideraciones importantes sobre paths
La opción paths no modifica las rutas en el JavaScript emitido. Si se usa paths, el bundler o el runtime deben estar configurados para resolver los mismos alias:
// TypeScript resuelve @modelos/usuario a src/modelos/usuario.ts
// Pero el JavaScript emitido mantiene: import { Usuario } from "@modelos/usuario"
// El bundler debe saber resolver @modelos/* -> src/modelos/*
Para proyectos con Webpack, la configuración equivalente sería:
// webpack.config.js
module.exports = {
resolve: {
alias: {
"@modelos": path.resolve(__dirname, "src/modelos"),
"@servicios": path.resolve(__dirname, "src/servicios"),
"@utils": path.resolve(__dirname, "src/utilidades")
}
}
}
Alternativa: package.json imports
Los imports en package.json son una alternativa estándar a paths que funciona tanto en TypeScript como en Node.js y bundlers:
{
"imports": {
"#modelos/*": "./src/modelos/*",
"#utils/*": "./src/utilidades/*"
}
}
import { Usuario } from "#modelos/usuario"
Esta opción es preferible a paths para librerías publicadas porque funciona en el runtime sin configuración adicional.
La opción moduleResolution
La relación entre las opciones module y moduleResolution determina el comportamiento completo del sistema de módulos:
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext"
}
}
Combinaciones recomendadas según el entorno:
Aplicaciones con bundler (Webpack, Vite, Rollup):
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler"
}
}
Node.js moderno (con ES modules):
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext"
}
}
Librerías npm (máxima compatibilidad):
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"declaration": true,
"strict": true
}
}
Para librerías publicadas en npm, nodenext es la opción más segura. Código que funciona con nodenext generalmente funciona en bundlers, pero lo contrario no siempre es cierto.
Extensiones .js en importaciones TypeScript
Con nodenext, las importaciones relativas en archivos ESM requieren la extensión .js, aunque el archivo fuente sea .ts:
// archivo: src/servicios/auth.ts
import { Usuario } from "../modelos/usuario.js"
import { hashPassword } from "../utils/crypto.js"
export function autenticar(nombre: string, password: string): boolean {
const hash = hashPassword(password)
return hash.length > 0
}
TypeScript realiza la sustitución de extensión automáticamente: cuando ve ./modelos/usuario.js, busca ./modelos/usuario.ts en el sistema de archivos.
Las extensiones .mjs y .cjs se corresponden con .mts y .cts:
// Importar un módulo ESM explícitamente
import { helper } from "./utils.mjs"
// TypeScript busca ./utils.mts
// Importar un módulo CJS explícitamente
import { legacy } from "./old-module.cjs"
// TypeScript busca ./old-module.cts
Interoperabilidad ESM y CJS
La interoperabilidad entre ES modules y CommonJS es uno de los aspectos más complejos del sistema de módulos de Node.js.
Importar CJS desde ESM
Los módulos ESM pueden importar módulos CommonJS, pero con restricciones:
// módulo-cjs.cts
export = {
versión: "1.0.0",
procesar(datos: string) {
return datos.toUpperCase()
}
}
// módulo-esm.mts
// Importacion por defecto del objeto exports completo
import módulo from "./módulo-cjs.cjs"
console.log(módulo.versión)
console.log(módulo.procesar("hola"))
La opción esModuleInterop
La opción esModuleInterop facilita la importación de módulos CJS con sintaxis ESM:
{
"compilerOptions": {
"esModuleInterop": true
}
}
Sin esModuleInterop, importar un módulo CJS con exportación por defecto requiere la sintaxis de namespace:
// Sin esModuleInterop
import * as express from "express"
const app = express()
// Con esModuleInterop
import express from "express"
const app = express()
verbatimModuleSyntax
La opción verbatimModuleSyntax es una alternativa moderna que garantiza que la sintaxis de importación escrita se preserva tal cual en la salida:
{
"compilerOptions": {
"verbatimModuleSyntax": true
}
}
Con está opción, import type se convierte en obligatorio para importaciones que solo se usan como tipos:
// Con verbatimModuleSyntax, esto es un error si Usuario solo se usa como tipo
// import { Usuario } from "./modelos"
// Correcto
import type { Usuario } from "./modelos"
import { crearUsuario } from "./modelos"
Project references
Las project references permiten dividir un proyecto grande en subproyectos con compilación independiente:
// tsconfig.json (raiz)
{
"references": [
{ "path": "./paquetes/comun" },
{ "path": "./paquetes/servidor" },
{ "path": "./paquetes/cliente" }
]
}
Cada subproyecto tiene su propio tsconfig.json con composite: true:
// paquetes/comun/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"outDir": "dist",
"rootDir": "src"
}
}
// paquetes/servidor/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"outDir": "dist",
"rootDir": "src"
},
"references": [
{ "path": "../comun" }
]
}
Las project references habilitan la compilación incremental: solo se recompilan los subproyectos que han cambiado. Se usa tsc --build para compilar respetando las dependencias.
Depuración de problemas de resolución
Cuando una importación no se resuelve correctamente, la opción --traceResolution muestra el proceso completo:
tsc --traceResolution
La salida detalla cada paso que sigue el compilador para localizar el archivo:
======== Resolving module './modelos/usuario' from '/src/app.ts'. ========
Module resolution kind is not specified, using 'Node10'.
Loading module as file / folder, candidate module location '/src/modelos/usuario'
File '/src/modelos/usuario.ts' exists - use it as a name resolution result.
======== Module name './modelos/usuario' was successfully resolved to '/src/modelos/usuario.ts'. ========
Problemas comunes
Módulo no encontrado: Verificar que la estrategia de resolución coincide con el entorno. Si se usa nodenext, las extensiones son obligatorias en ESM.
Tipos no detectados: Comprobar que typeRoots incluye la ubicación de los archivos .d.ts o que el paquete @types está instalado como dependencia de desarrollo.
Alias no resueltos en runtime: Recordar que paths no transforma las rutas en el JavaScript emitido. El bundler o runtime debe estar configurado para resolver los mismos alias.
Campo exports no respetado: Las estrategias node (antigua) y classic no soportan el campo exports de package.json. Cambiar a nodenext o bundler.
La opción moduleDetection: "force" trata todos los archivos como módulos, lo que puede resolver problemas con archivos que TypeScript interpreta incorrectamente como scripts:
{
"compilerOptions": {
"moduleDetection": "force"
}
}
La correcta configuración de la resolución de módulos es fundamental para la experiencia de desarrollo, el rendimiento de compilación y la portabilidad del código entre diferentes entornos de ejecución.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en TypeScript
Documentación oficial de TypeScript
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, TypeScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de TypeScript
Explora más contenido relacionado con TypeScript y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Comprender las estrategias de resolución de módulos (node, bundler, nodenext), configurar path mapping con paths y baseUrl, conocer las project references para monorepos, dominar la opción moduleResolution, resolver extensiones .js en ESM, y gestionar la interoperabilidad ESM/CJS.