require vs import in TypeScript: A Step-by-Step Tutorial for the Confused

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

require(x) vs import x: Why This Is a Perpetual Headache for TypeScript Developers

\n\n

If you've ever written TypeScript and seen an error like Cannot use import statement outside a module or require is not defined — you are not alone. On Stack Overflow, the question "The difference between 'require(x)' and 'import x'" has gathered tens of thousands of views. And this is no coincidence: confusion between the two module systems is one of the most common pitfalls that even experienced developers fall into.

\n\n

In this tutorial, we won't just list the differences. We will go from complete misunderstanding to confidently choosing the correct syntax. You will learn how TypeScript compiles your code, why require is still alive, and when you should use import.

\n\n

1. Historical Context: Where the Two Systems Came From

\n\n

To understand the difference, you need to look into the history of JavaScript. Initially, Node.js did not have a built-in module system. Developers invented CommonJS — a synchronous way to include modules via require(). This approach works server-side, but was not suitable for browsers — they needed asynchronous loading.

\n\n

Later, the ES Modules (ESM) standard appeared — the official ECMAScript module syntax. TypeScript, as a superset of JS, supports both options. But this is exactly where the confusion begins: you write import, and the compiler turns it into require — or vice versa.

\n\n

2. Syntax: The Main Differences

\n\n

Let's break down what both approaches look like.

\n\n

CommonJS (require)

\n\n
// Importing an entire module\nconst fs = require('fs');\n\n// Importing part of a module (destructuring)\nconst { readFile, writeFile } = require('fs');\n\n// Export\nmodule.exports = { myFunction, myVariable };\nexports.myFunction = () => { ... };
\n\n

ES Modules (import)

\n\n
// Importing an entire module\nimport fs from 'fs';\n\n// Importing named exports\nimport { readFile, writeFile } from 'fs';\n\n// Importing everything as an object\nimport * as fs from 'fs';\n\n// Export\nexport const myVariable = 42;\nexport function myFunction() { ... }\nexport default class MyClass { }
\n\n

At first glance, the difference is only in syntax. But let's dig deeper.

\n\n

3. Static vs Dynamic Import

\n\n

This is the key difference that affects how your code works.

\n\n
    \n
  • require — dynamic. You can call it anywhere, inside a condition or a function. The module is loaded at the time of the call.
  • \n
  • import — static. Imports are resolved at compile time, before code execution. They must always be at the top level of the file.
  • \n
\n\n

Example of dynamic require:

\n\n
let myModule;\nif (process.env.NODE_ENV === 'production') {\n  myModule = require('./production-config');\n} else {\n  myModule = require('./dev-config');\n}
\n\n

This trick won't work with import — it will be a syntax error. But for this, there is dynamic import() (returns a Promise):

\n\n
if (condition) {\n  const module = await import('./some-module');\n  module.doSomething();\n}
\n\n

4. How TypeScript Compiles Modules

\n\n

It all depends on the module setting in tsconfig.json. Here are the main options:

\n\n
    \n
  • "module": "commonjs" — your import will be transformed into require. The code will work in Node.js without additional configuration.
  • \n
  • "module": "esnext" — import will remain as is. Suitable for modern browsers or bundlers (Webpack, Vite).
  • \n
  • "module": "node16" or "nodenext" — TypeScript itself determines

Blogs

Book Recommendations