require(x) vs import x: por que isso é uma eterna dor de cabeça para desenvolvedores TypeScript
\n\nSe você já escreveu em TypeScript e viu um erro como Cannot use import statement outside a module ou require is not defined — você não está sozinho. No Stack Overflow, a pergunta "The difference between 'require(x)' and 'import x'" acumulou dezenas de milhares de visualizações. E isso não é por acaso: a confusão entre os dois sistemas de módulos é uma das armadilhas mais comuns em que até desenvolvedores experientes caem.
Neste tutorial, não vamos apenas listar as diferenças. Vamos percorrer o caminho da total incompreensão até a escolha confiante da sintaxe correta. Você aprenderá como o TypeScript compila seu código, por que o require ainda existe e quando usar import.
1. Contexto histórico: de onde vieram os dois sistemas
\n\nPara entender a diferença, precisamos olhar para a história do JavaScript. Inicialmente, o Node.js não tinha um sistema de módulos embutido. Os desenvolvedores criaram o CommonJS — uma forma síncrona de conectar módulos via require(). Essa abordagem funciona no lado do servidor, mas não era adequada para navegadores — que precisam de carregamento assíncrono.
Mais tarde, surgiu o padrão ES Modules (ESM) — a sintaxe oficial de módulos do ECMAScript. O TypeScript, como uma extensão do JS, suporta ambas as opções. Mas é aqui que começa a confusão: você escreve import, e o compilador o transforma em require — ou vice-versa.
2. Sintaxe: principais diferenças
\n\nVamos detalhar como cada abordagem se parece.
\n\nCommonJS (require)
\n\n// Importação de módulo inteiro\nconst fs = require('fs');\n\n// Importação de parte do módulo (desestruturação)\nconst { readFile, writeFile } = require('fs');\n\n// Exportação\nmodule.exports = { myFunction, myVariable };\nexports.myFunction = () => { ... };\n\nES Modules (import)
\n\n// Importação de módulo inteiro\nimport fs from 'fs';\n\n// Importação de exportações nomeadas\nimport { readFile, writeFile } from 'fs';\n\n// Importação de tudo como objeto\nimport * as fs from 'fs';\n\n// Exportação\nexport const myVariable = 42;\nexport function myFunction() { ... }\nexport default class MyClass { }\n\nÀ primeira vista, a diferença é apenas na sintaxe. Mas vamos mais fundo.
\n\n3. Importação estática vs dinâmica
\n\nEsta é a diferença chave que afeta como seu código funciona.
\n\n- \n
require— dinâmico. Você pode chamá-lo em qualquer lugar, dentro de uma condição ou função. O módulo é carregado no momento da chamada. \n import— estático. As importações são resolvidas em tempo de compilação, antes da execução do código. Elas devem sempre estar no nível superior do arquivo. \n
Exemplo 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\nEsse truque não funciona com import — resultará em um erro de sintaxe. Mas para isso existe o import() dinâmico (retorna uma Promise):
if (condition) {\n const module = await import('./some-module');\n module.doSomething();\n}\n\n4. Como o TypeScript compila módulos
\n\nTudo depende da configuração module no tsconfig.json. Aqui estão as principais opções:
- \n
"module": "commonjs"— seuimportserá transformado emrequire. O código funcionará no Node.js sem configurações adicionais. \n "module": "esnext"— oimportpermanecerá como está. Adequado para navegadores modernos ou bundlers (Webpack, Vite). \n "module": "node16"ou"nodenext"— o TypeScript determina automaticamente