require(x) vs import x: por qué es un dolor de cabeza eterno para el desarrollador de TypeScript
\n\nSi alguna vez has escrito en TypeScript y has visto un error como Cannot use import statement outside a module o require is not defined — no estás solo. En Stack Overflow, la pregunta "The difference between 'require(x)' and 'import x'" ha acumulado decenas de miles de visitas. Y no es casualidad: la confusión entre los dos sistemas de módulos es una de las trampas más comunes en las que caen incluso desarrolladores experimentados.
En este tutorial no solo enumeraremos las diferencias. Recorreremos el camino desde la total incomprensión hasta la elección segura de la sintaxis correcta. Aprenderás cómo TypeScript compila tu código, por qué require sigue vivo y cuándo deberías usar import.
1. Contexto histórico: de dónde surgieron los dos sistemas
\n\nPara entender la diferencia, hay que mirar la historia de JavaScript. Originalmente, Node.js no tenía un sistema de módulos incorporado. Los desarrolladores inventaron CommonJS — una forma síncrona de conectar módulos mediante require(). Este enfoque funciona del lado del servidor, pero no era adecuado para navegadores — allí se necesita carga asíncrona.
Más tarde surgió el estándar ES Modules (ESM) — la sintaxis de módulos oficial de ECMAScript. TypeScript, como superconjunto de JS, soporta ambas opciones. Pero aquí es donde comienza la confusión: escribes import, y el compilador lo convierte en require — o viceversa.
2. Sintaxis: diferencias principales
\n\nVamos a desglosar cómo se ven ambos enfoques.
\n\nCommonJS (require)
\n\n// Importar un módulo completo\nconst fs = require('fs');\n\n// Importar parte de un módulo (desestructuración)\nconst { readFile, writeFile } = require('fs');\n\n// Exportar\nmodule.exports = { myFunction, myVariable };\nexports.myFunction = () => { ... };\n\nES Modules (import)
\n\n// Importar un módulo completo\nimport fs from 'fs';\n\n// Importar exportaciones nombradas\nimport { readFile, writeFile } from 'fs';\n\n// Importar todo como un objeto\nimport * as fs from 'fs';\n\n// Exportar\nexport const myVariable = 42;\nexport function myFunction() { ... }\nexport default class MyClass { }\n\nA primera vista, la diferencia es solo en la sintaxis. Pero profundicemos.
\n\n3. Importación estática vs dinámica
\n\nEsta es la diferencia clave que afecta cómo funciona tu código.
\n\n- \n
require— dinámico. Puedes llamarlo en cualquier lugar, dentro de una condición o función. El módulo se carga en el momento de la llamada. \n import— estático. Las importaciones se resuelven en la etapa de compilación, antes de ejecutar el código. Siempre deben estar en el nivel superior del archivo. \n
Ejemplo de require dinámico:
\n\nlet myModule;\nif (process.env.NODE_ENV === 'production') {\n myModule = require('./production-config');\n} else {\n myModule = require('./dev-config');\n}\n\nEse truco no funcionará con import — dará un error de sintaxis. Pero para eso existe import() dinámico (devuelve una Promise):
if (condition) {\n const module = await import('./some-module');\n module.doSomething();\n}\n\n4. Cómo compila TypeScript los módulos
\n\nTodo depende de la configuración module en tsconfig.json. Estas son las opciones principales:
- \n
"module": "commonjs"— tuimportse convertirá enrequire. El código funcionará en Node.js sin configuraciones adicionales. \n "module": "esnext"—importse mantendrá tal cual. Adecuado para navegadores modernos o empaquetadores (Webpack, Vite). \n "module": "node16"o"nodenext"— TypeScript determina automáticamente \n