require(x) vs import x : pourquoi c'est un casse-tête récurrent pour les développeurs TypeScript
\n\nSi vous avez déjà codé en TypeScript et rencontré une erreur comme Cannot use import statement outside a module ou require is not defined, vous n'êtes pas seul. Sur Stack Overflow, la question "The difference between 'require(x)' and 'import x'" cumule des dizaines de milliers de vues. Et ce n'est pas un hasard : la confusion entre les deux systèmes de modules est l'un des pièges les plus fréquents, même pour les développeurs expérimentés.
Dans ce tutoriel, nous ne nous contenterons pas de lister les différences. Nous allons passer de l'incompréhension totale à un choix éclairé de la bonne syntaxe. Vous découvrirez comment TypeScript compile votre code, pourquoi require est toujours d'actualité et quand utiliser import.
1. Contexte historique : d'où viennent ces deux systèmes
\n\nPour comprendre la différence, il faut remonter dans l'histoire de JavaScript. À l'origine, Node.js n'avait pas de système de modules intégré. Les développeurs ont créé CommonJS — une méthode synchrone pour inclure des modules via require(). Cette approche fonctionne côté serveur, mais pas pour les navigateurs, qui nécessitent un chargement asynchrone.
Plus tard, le standard ES Modules (ESM) est apparu — la syntaxe modulaire officielle d'ECMAScript. TypeScript, en tant que surcouche de JS, prend en charge les deux options. Mais c'est là que la confusion commence : vous écrivez import, et le compilateur le transforme en require — ou l'inverse.
2. Syntaxe : les principales différences
\n\nDétaillons à quoi ressemblent les deux approches.
\n\nCommonJS (require)
\n\n// Import d'un module entier\nconst fs = require('fs');\n\n// Import d'une partie d'un module (destructuration)\nconst { readFile, writeFile } = require('fs');\n\n// Export\nmodule.exports = { myFunction, myVariable };\nexports.myFunction = () => { ... };\n\nES Modules (import)
\n\n// Import d'un module entier\nimport fs from 'fs';\n\n// Import d'exports nommés\nimport { readFile, writeFile } from 'fs';\n\n// Import de tout comme un objet\nimport * as fs from 'fs';\n\n// Export\nexport const myVariable = 42;\nexport function myFunction() { ... }\nexport default class MyClass { }\n\nÀ première vue, seule la syntaxe diffère. Mais creusons un peu plus.
\n\n3. Import statique vs dynamique
\n\nC'est la différence clé qui impacte le fonctionnement de votre code.
\n\n- \n
require— dynamique. Vous pouvez l'appeler n'importe où, dans une condition ou une fonction. Le module est chargé au moment de l'appel. \n import— statique. Les imports sont résolus à la compilation, avant l'exécution du code. Ils doivent toujours être en haut du fichier. \n
Exemple de require dynamique :
\n\nlet myModule;\nif (process.env.NODE_ENV === 'production') {\n myModule = require('./production-config');\n} else {\n myModule = require('./dev-config');\n}\n\nUne telle astuce ne fonctionne pas avec import — cela génère une erreur de syntaxe. Mais il existe import() dynamique (qui retourne une Promise) :
if (condition) {\n const module = await import('./some-module');\n module.doSomething();\n}\n\n4. Comment TypeScript compile les modules
\n\nTout dépend du paramètre module dans tsconfig.json. Voici les options principales :
- \n
"module": "commonjs"— votreimportsera transformé enrequire. Le code fonctionnera dans Node.js sans configuration supplémentaire. \n "module": "esnext"—importreste tel quel. Idéal pour les navigateurs modernes ou les bundlers (Webpack, Vite). \n "module": "node16"ou"nodenext"— TypeScript détecte automatiquement