Resolución de módulos en TypeScript

Avanzado
TypeScript
TypeScript
Actualizado: 18/04/2026

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.

Estrategias de resolución de módulos

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 ../):

  1. Busca el archivo con extensión .ts, .tsx, .d.ts
  2. Busca un directorio con package.json que tenga campo types o main
  3. Busca un archivo index.ts, index.tsx o index.d.ts en 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 ./):

  1. Busca en node_modules del directorio actual
  2. Sube un nivel y busca en el siguiente node_modules
  3. 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 .js en 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 .ts correspondiente.

Detección del formato de módulo

Con nodenext, TypeScript determina si cada archivo es ESM o CJS según estas reglas:

  • Archivos .mts / .mjs son siempre ESM
  • Archivos .cts / .cjs son siempre CJS
  • Archivos .ts / .js dependen del campo "type" en el package.json más cercano
    • "type": "module" implica ESM
    • "type": "commonjs" o sin campo type implica 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 - Autor del tutorial

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.