En el ecosistema JavaScript moderno, publicar librerías profesionales exige dar soporte a un modelo de ejecución dual: el paquete debe consumirse sin fisuras tanto en entornos clásicos CommonJS (CJS) como en módulos nativos ECMAScript (ESM) y empaquetadores modernos (Vite, Next.js, Rspack).
Los archivos de declaración de tipos (.d.ts) constituyen la interfaz externa de una librería. Una emisión de declaraciones defectuosa acarrea graves problemas arquitectónicos:
moduleResolution: "nodenext") cuando las extensiones de tipos (.d.mts frente a .d.cts) no coinciden con el formato del módulo en runtime."types" se ubica detrás de las condiciones "import" o "require" en package.json.El mapa de exportación de un paquete moderno asocia los archivos de runtime con sus declaraciones mediante una matriz estructurada de condiciones (Conditional Exports):
+-------------------------------------------------------------------------+
| ARQUITECTURA DE PAQUETE DUAL (CJS + ESM) |
+-------------------------------------------------------------------------+
| package.json: |
| "exports": { |
| ".": { |
| "types": { |
| "import": "./dist/index.d.mts", <-- Tipos estrictos para ESM |
| "require": "./dist/index.d.cts" <-- Tipos estrictos para CommonJS|
| }, |
| "import": "./dist/index.mjs", <-- Código ejecutable ESM |
| "require": "./dist/index.cjs" <-- Código ejecutable CommonJS |
| } |
| } |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| VINCULACIÓN DE CÓDIGO FUENTE (DECLARATION MAPS) |
| dist/index.d.ts.map: |
| Asocia el archivo .d.ts directamente con src/index.ts. |
| La función "Go to Definition" del IDE salta al código TypeScript |
| original comentado en lugar de a un archivo compilado de firmas. |
+-------------------------------------------------------------------------+Regla fundamental de precedencia en package.json: la condición "types" debe situarse siempre en primer lugar dentro del bloque de exportación. Si el compilador se encuentra con "import" antes que "types", puede resolver el archivo en modo sin tipos o descartar las declaraciones por completo.
declarationMap: true sin tener que distribuir directorios de código pesados en el bundle de producción.process.env, enriquecimiento del contexto de sesión en librerías de autenticación (NextAuth/Auth.js) o añadido de atributos personalizados en JSX.package.json y la resolución de tipos con @arethetypeswrong/cli (attw) para impedir publicaciones rotas.Al utilizar herramientas de empaquetado modernas (tsup, Rollup, esbuild), la generación de tipos se delega a tsc mediante --emitDeclarationOnly:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": true,
"outDir": "./dist/types",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"]
}declaration: true: Genera los archivos .d.ts.declarationMap: true: Genera los archivos sourcemap .d.ts.map que enlazan las declaraciones con el código original en src/. Esto permite que pulsar F12 ("Go to Definition") lleve directamente al código legible y documentado en lugar de a un stub abstracto.A continuación se muestra la estructura canónica de package.json que satisface las especificaciones estrictas de Node.js y TypeScript 5+:
{
"name": "@enterprise/telemetry-sdk",
"version": "2.4.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": {
"import": "./dist/index.d.mts",
"require": "./dist/index.d.cts"
},
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./package.json": "./package.json"
},
"files": [
"dist"
],
"scripts": {
"build": "tsup src/index.ts --format esm,cjs --dts",
"prepublishOnly": "attw --pack ."
}
}@arethetypeswrong/cli (attw)#La utilidad @arethetypeswrong/cli simula el comportamiento en los distintos entornos de consumo (Node10, Node16 CJS, Node16 ESM, Bundler) detectando anomalías de extensión o desajustes en el mapeo de tipos antes del release.
pnpm dlx @arethetypeswrong/cli --pack .Matriz de resultados de attw ante una configuración correcta:
┌───────────────────┬──────────────┬──────────────┬─────────────┐
│ Entrypoint │ node10 │ node16 (esm) │ bundler │
├───────────────────┼──────────────┼──────────────┼─────────────┤
│ . (import) │ 🟢 (esm) │ 🟢 (esm) │ 🟢 (esm) │
│ . (require) │ 🟢 (cjs) │ 🟢 (cjs) │ 🟢 (cjs) │
└───────────────────┴──────────────┴──────────────┴─────────────┘
All checks passed! Package is ready for publication.Cuando una librería debe permitir a sus consumidores inyectar tipos propios en contratos ya existentes, se recurre a la técnica de Module Augmentation.
// Extensión del registro global de eventos de la librería
import '@enterprise/telemetry-sdk';
// Aumentar las declaraciones del módulo externo
declare module '@enterprise/telemetry-sdk' {
// TypeScript realiza fusión automática de interfaces (Declaration Merging)
export interface CustomEventRegistry {
'user.login': { userId: string; timestamp: number; ipAddress: string };
'payment.success': { orderId: string; amountCents: number; currency: 'USD' | 'EUR' };
}
}
// Extensión del contexto global NodeJS ProcessEnv
declare global {
namespace NodeJS {
interface ProcessEnv {
TELEMETRY_ENDPOINT: string;
TELEMETRY_API_KEY: string;
LOG_LEVEL: 'debug' | 'info' | 'warn' | 'error';
}
}
}
// El archivo debe ser un módulo (contener al menos un import o export)
export {};Generación de bundles duales (index.js, index.cjs) y archivos de tipos (index.d.mts, index.d.cts).
Validación de sintaxis de exports en package.json, rutas de archivo y coherencia de enlaces.
Simulación de importación en Node16 ESM/CJS y Bundler comprobando la accesibilidad de tipos.
Envío del archivo tarball verificado al registro con metadatos inmutables.
A continuación se muestra una configuración completa de tsup optimizada para compilar un paquete dual ESM/CJS generando automáticamente sus declaraciones .d.mts y .d.cts:
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
// Generación automática de archivos .d.ts adaptados a .d.mts y .d.cts
dts: {
compilerOptions: {
declarationMap: true,
},
},
splitting: false,
sourcemap: true,
clean: true,
minify: false,
target: 'es2022',
outExtension({ format }) {
return {
js: format === 'esm' ? '.js' : '.cjs',
};
},
// Mantener peer dependencies externas
external: ['react', 'react-dom'],
});Si en el bloque "exports" de package.json la propiedad "types" figura después de las condiciones de importación, el algoritmo de resolución de TypeScript en diversos entornos coincidirá con "import" y cerrará la evaluación antes de registrar los tipos.
// ❌ Antipatrón: types se ubica tras import (¡las declaraciones serán ignoradas!)
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
}
// ✅ Práctica recomendada: types DEBE IR SIEMPRE en primera posición
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
}Definir un archivo de declaraciones sin sentencias import o export en su nivel superior lo convierte en un script ambient global. Sus declaraciones escaparán a todo el proyecto del consumidor, contaminando el ámbito global y provocando colisiones de identificadores.
Solución: añadir siempre un export {}; al final de cualquier archivo que defina bloques declare module o declare global.
Si en tsconfig se activa declarationMap: true, pero se omite la carpeta src/ del listado "files" en package.json, la acción "Go to Definition" del IDE intentará abrir un archivo inexistente, lanzando un error File not found.
Solución: incluir la carpeta src/ en el paquete publicado o deshabilitar declarationMap para releases públicos.
Ajustar tsup o tsc para generar declaraciones duales (.d.mts para ESM y .d.cts/.d.ts para CJS).
Definir las condiciones de exportación en package.json situando la clave 'types' al inicio.
Ejecutar pnpm dlx publint para corregir fallos sintácticos y rutas erróneas en package.json.
Ejecutar pnpm dlx @arethetypeswrong/cli --pack . y corroborar la ausencia de advertencias rojas.
Lanzar pnpm publish con autenticación 2FA y metadatos de proveniencia verificados.