Resolución de módulos y package exports en TypeScript | Nicolás Garzón
moduleResolution define cómo TypeScript convierte un specifier en un archivo con tipos, intentando imitar al runtime o bundler que ejecutará el código.
TypeScript
Copiar import { createProduct } from "./product.js" ;
import { z } from "zod" ;
Texto
Copiar TypeScript → encuentra .ts, .tsx o .d.ts
runtime/bundler → encuentra JavaScript ejecutableEl checker puede sustituir extensiones para obtener tipos mientras conserva el specifier que utilizará el runtime.
Para proyectos procesados por Vite, Next.js, esbuild u otro bundler que soporta package exports y specifiers modernos sin todas las restricciones de Node ESM.
Modelan resolución moderna de Node.js, incluyendo el formato ESM/CommonJS determinado por extensión y package.json.
nodenext sigue las capacidades actuales de Node; node16 fija una generación de reglas más estable.
Modo heredado para CommonJS antiguo. No representa correctamente exports modernos.
Ambos deben representar la herramienta real.
JSON
Copiar {
"compilerOptions" : {
"module" : "NodeNext" ,
"moduleResolution" : "NodeNext"
}
} JSON
Copiar {
"compilerOptions" : {
"module" : "ESNext" ,
"moduleResolution" : "Bundler" ,
"noEmit" : true
}
} TypeScript
Copiar import { helper } from "./helper.js" ; Aunque el archivo fuente sea helper.ts, el specifier describe el archivo JavaScript que existirá después de compilar.
Bundlers pueden aceptar ./helper y resolverlo, pero no extrapoles esa regla a Node ESM nativo.
JSON
Copiar {
"type" : "module"
} En Node determina cómo interpretar archivos .js dentro del package scope.
.mts emite .mjs.
.cts emite .cjs.
.ts depende de type y configuración en modos Node modernos.
JSON
Copiar {
"exports" : {
"." : {
"types" : "./dist/index.d.ts" ,
"import" : "./dist/index.js" ,
"require" : "./dist/index.cjs"
} ,
"./testing" : {
"types" : "./dist/testing.d.ts" ,
"import" : "./dist/testing.js"
}
}
} Controla la superficie pública y condiciones por consumidor.
La resolución evalúa condiciones según reglas del host. Mantén types antes de las condiciones runtime en mapas donde las herramientas lo requieran y verifica el package generado.
JSON
Copiar {
"types" : "./dist/index.d.ts"
} Declara la entrada principal de tipos en paquetes simples. exports puede definir entradas y tipos por subpath.
Permite ofrecer declarations diferentes según versión de TypeScript.
JSON
Copiar {
"typesVersions" : {
">=5.0" : {
"*" : [ "ts5/*" ]
}
}
} Es una herramienta de compatibilidad de librerías; no debe usarse si una sola declaración portable basta.
JSON
Copiar {
"compilerOptions" : {
"baseUrl" : "." ,
"paths" : {
"@/*" : [ "src/*" ]
}
}
} paths informa al checker. No reescribe imports emitidos ni configura automáticamente Node o el bundler. La herramienta runtime debe conocer el mismo alias.
Permite que TypeScript trate varios directorios como una estructura virtual unificada, útil para código generado y fuente. No mueve archivos durante emisión.
JSON
Copiar {
"imports" : {
"#logger" : "./src/logger.js"
}
} Node y TypeScript moderno pueden resolver aliases internos definidos por el paquete.
Bash
Copiar npx tsc --traceResolution Muestra cada paso de resolución. Es útil cuando editor, build y runtime encuentran archivos distintos.
Bash
Copiar npx tsc --explainFiles Explica por qué un archivo pertenece al programa.
Dos copias de una librería pueden producir incompatibilidad cuando existen clases con private/protected, unique symbols o tipos nominales.
Revisa lockfile, peerDependencies y paths físicos.
TypeScript
Copiar import styles from "./styles.module.css" ; El bundler conoce el recurso; TypeScript necesita una declaración compatible:
TypeScript
Copiar declare module "*.module.css" {
const classes: Record< string , string > ;
export default classes;
} JSON
Copiar {
"name" : "@example/core" ,
"type" : "module" ,
"exports" : {
"." : {
"types" : "./dist/index.d.ts" ,
"import" : "./dist/index.js"
}
} ,
"files" : [ "dist" ]
}
Elegir moduleResolution por costumbre y no por runtime.
Usar paths esperando reescritura de JavaScript.
Omitir extensión .js en Node ESM.
Publicar exports sin la declaration correspondiente.
Permitir imports a rutas internas no públicas.
Mezclar dos copias de tipos nominales.
Configurar NodeNext con sintaxis incompatible con package type.
Declarar módulos de assets sin que el bundler los procese realmente.
moduleResolution debe imitar al host real.
Bundler y NodeNext tienen reglas diferentes.
TypeScript sustituye extensiones para encontrar tipos.
package exports define la API pública.
paths solo afecta resolución estática.
Los aliases runtime necesitan configuración equivalente.
traceResolution ayuda a investigar.
Publicar una librería exige alinear JS, .d.ts y package.json.
¿Por qué un alias en paths puede compilar y fallar al ejecutar en Node?
Respuesta Porque paths solo enseña al checker dónde buscar; no cambia el specifier emitido ni configura el resolvedor de Node.
Declaration files .d.ts describe JavaScript existente sin generar implementación.