Introducción: Por qué esModuleInterop no es solo una casilla en tsconfig
\n\nSi alguna vez has escrito en TypeScript e importado una librería como express o lodash, seguramente te has encontrado con el momento en que el compilador se queja de import * as express from 'express'. O, por el contrario, usas tranquilamente import express from 'express', pero en el código JavaScript generado ves una extraña construcción module.exports.default. ¿Qué magia es esta? Pues la magia (o su ausencia) es la bandera esModuleInterop en tu tsconfig.json.
Este parámetro es una de las preguntas más populares en Stack Overflow (¡miles de visitas!), y no es de extrañar: sin entenderlo, es fácil caer en la trampa de la incompatibilidad de módulos. En este tutorial no solo explicaremos la teoría, sino que recorreremos el camino desde el problema hasta la solución con ejemplos prácticos. ¡Allá vamos!
\n\nEl problema: Conflicto de dos mundos de módulos
\n\nTypeScript es una extensión de JavaScript, y JavaScript históricamente tiene dos sistemas principales de módulos:
\n- \n
- CommonJS (CJS) — el estándar de Node.js, usa
require()ymodule.exports. Ejemplo:const express = require('express');\n - ES Modules (ESM) — el estándar moderno, usa
importyexport. Ejemplo:import express from 'express';\n
El problema es que estos sistemas manejan de manera diferente la exportación por defecto (default export). En ESM, un módulo puede tener un export default, y se importa como import name from 'module'. En CommonJS no existe el concepto de "exportación por defecto" — solo existe el objeto module.exports, que puede ser cualquier cosa.
Cuando TypeScript compila tu código a CommonJS (lo que a menudo se necesita para Node.js), debe decidir: ¿cómo representar import x from 'y' en forma de require()? Sin esModuleInterop, TypeScript lo hace literalmente: crea module.exports.default, lo que rompe la importación de librerías escritas para CommonJS.
¿Qué es esModuleInterop?
\n\nesModuleInterop es una bandera en tsconfig.json que habilita compatibilidad adicional entre CommonJS y ES Modules. Cuando se establece en true, TypeScript genera código auxiliar que permite escribir import foo from 'bar' incluso para módulos CommonJS que no tienen una exportación default.
Por defecto en TypeScript 4.x y superiores, esta bandera es true (si usas --init), pero en proyectos antiguos a menudo se desactivaba. ¿Por qué? Porque cambia el comportamiento de la importación y, sin entenderlo, provoca errores.
Cómo se ve en tsconfig.json
\n{\n "compilerOptions": {\n "esModuleInterop": true,\n // otras configuraciones...\n }\n}\n\n\nActivar esta bandera también activa automáticamente allowSyntheticDefaultImports, que permite la importación por defecto desde módulos sin una exportación default real a nivel de tipos.
Tutorial paso a paso: del problema a la solución
\n\nAnalicemos la situación con un ejemplo real. Supongamos que tenemos una librería simple en CommonJS:
\n\n// myLib.js (CommonJS)\nmodule.exports = function greet(name) {\n return `¡Hola, ${name}!`;\n};\n\n\nQueremos usarla en un proyecto TypeScript. Creemos un archivo index.ts:
// index.ts\nimport greet from './myLib';\nconsole.log(greet('Mundo'));\n\n\nAhora veamos qué sucede al compilar con diferentes configuraciones de esModuleInterop.
Paso 1: Sin esModuleInterop (false)
\n\nEjecutemos la compilación con "esModuleInterop": false. TypeScript mostrará un error:
El módulo '"myLib"' no tiene ninguna exportación por defecto.