require(x) vs import x: Warum das der ewige Kopfschmerz eines TypeScript-Entwicklers ist
\n\nWenn Sie jemals in TypeScript geschrieben und einen Fehler wie Cannot use import statement outside a module oder require is not defined gesehen haben – Sie sind nicht allein. Auf Stack Overflow hat die Frage "The difference between 'require(x)' and 'import x'" Zehntausende von Aufrufen gesammelt. Und das ist kein Zufall: Die Verwirrung zwischen den beiden Modulsystemen ist eine der häufigsten Fallen, in die selbst erfahrene Entwickler tappen.
In diesem Tutorial werden wir nicht nur die Unterschiede auflisten. Wir werden den Weg von völligem Unverständnis bis zur sicheren Wahl der richtigen Syntax gehen. Sie werden erfahren, wie TypeScript Ihren Code kompiliert, warum require immer noch lebt und wann Sie import verwenden sollten.
1. Historischer Kontext: Woher die beiden Systeme kommen
\n\nUm den Unterschied zu verstehen, müssen wir in die Geschichte von JavaScript eintauchen. Ursprünglich hatte Node.js kein eingebautes Modulsystem. Entwickler erfanden CommonJS – eine synchrone Methode, um Module über require() einzubinden. Dieser Ansatz funktioniert serverseitig, war aber für Browser ungeeignet – dort war asynchrones Laden erforderlich.
Später kam der Standard ES Modules (ESM) – die offizielle Modulsyntax von ECMAScript. TypeScript, als Erweiterung von JS, unterstützt beide Varianten. Aber genau hier beginnt die Verwirrung: Sie schreiben import, und der Compiler wandelt es in require um – oder umgekehrt.
2. Syntax: Die Hauptunterschiede
\n\nLassen Sie uns aufschlüsseln, wie beide Ansätze aussehen.
\n\nCommonJS (require)
\n\n// Import eines gesamten Moduls\nconst fs = require('fs');\n\n// Import eines Teils eines Moduls (Destrukturierung)\nconst { readFile, writeFile } = require('fs');\n\n// Export\nmodule.exports = { myFunction, myVariable };\nexports.myFunction = () => { ... };\n\nES Modules (import)
\n\n// Import eines gesamten Moduls\nimport fs from 'fs';\n\n// Import benannter Exporte\nimport { readFile, writeFile } from 'fs';\n\n// Import von allem als Objekt\nimport * as fs from 'fs';\n\n// Export\nexport const myVariable = 42;\nexport function myFunction() { ... }\nexport default class MyClass { }\n\nAuf den ersten Blick besteht der Unterschied nur in der Syntax. Aber schauen wir tiefer.
\n\n3. Statischer vs dynamischer Import
\n\nDies ist der entscheidende Unterschied, der beeinflusst, wie Ihr Code funktioniert.
\n\n- \n
require– dynamisch. Sie können es überall aufrufen, innerhalb einer Bedingung oder Funktion. Das Modul wird zum Zeitpunkt des Aufrufs geladen. \n import– statisch. Importe werden zur Kompilierzeit aufgelöst, bevor der Code ausgeführt wird. Sie müssen immer auf der obersten Ebene der Datei stehen. \n
Beispiel für dynamisches require:
\n\nlet myModule;\nif (process.env.NODE_ENV === 'production') {\n myModule = require('./production-config');\n} else {\n myModule = require('./dev-config');\n}\n\nSo ein Trick funktioniert mit import nicht – es gäbe einen Syntaxfehler. Aber dafür gibt es den dynamischen import() (gibt ein Promise zurück):
if (condition) {\n const module = await import('./some-module');\n module.doSomething();\n}\n\n4. Wie TypeScript Module kompiliert
\n\nAlles hängt von der Einstellung module in der tsconfig.json ab. Hier sind die wichtigsten Optionen:
- \n
"module": "commonjs"– Ihrimportwird zurequire. Der Code funktioniert in Node.js ohne zusätzliche Einstellungen. \n "module": "esnext"–importbleibt wie es ist. Geeignet für moderne Browser oder Bundler (Webpack, Vite). \n "module": "node16"oder"nodenext"– TypeScript entscheidet selbst