JavaScript has two module systems in active use: ES Modules (ESM) and CommonJS (CJS). TypeScript can output either, and the choice affects how imports, exports, and file resolution work.
CommonJS
CommonJS is the original Node.js module system. It uses require() and module.exports:
// CJS
const fs = require("fs");
const { join } = require("path");
module.exports = { readFiles };
TypeScript configuration:
{
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "node"
}
}
ES Modules (ESM)
ES Modules are the standard module system, using import and export:
// ESM
import { readFileSync } from "fs";
import { join } from "path";
export { readFiles };
TypeScript configuration:
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler"
}
}
For native Node.js ESM, use:
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}
Key differences
| | CommonJS | ESM |
|---|---|---|
| Syntax | require() / module.exports | import / export |
| Loading | Synchronous | Asynchronous |
| Top-level await | Not supported | Supported |
| File extension | .js | .js (with "type": "module" in package.json) or .mjs |
| Dynamic imports | require(variable) works | Requires import() |
| Tree shaking | Not supported | Supported by bundlers |
The .ts file extension behavior
TypeScript files always use .ts (or .tsx for JSX). The output extension depends on your configuration:
module: CommonJS→ outputs.jswith CJS syntaxmodule: ESNext→ outputs.jswith ESM syntaxmodule: NodeNext→ outputs.jsor.mjsdepending onpackage.json
Which to choose
- Frontend projects — ESM with
moduleResolution: "bundler". Bundlers expect ESM. - Node.js backends — ESM with
module: "NodeNext"for new projects. CommonJS for legacy projects. - Libraries — publish both ESM and CommonJS for maximum compatibility.
- Tooling/scripts — CommonJS is fine for simple Node.js scripts.
Dual publishing for libraries
Libraries that want to support both module systems typically build twice:
{
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}
The exports field in package.json tells bundlers and runtimes which file to use based on the module system.
What to carry forward
- CommonJS uses
require(); ESM usesimport/export - ESM supports top-level await, tree shaking, and async loading
- frontend projects should use ESM with
moduleResolution: "bundler" - Node.js projects should use ESM with
module: "NodeNext"for new projects - libraries should publish both ESM and CommonJS
- the
exportsfield inpackage.jsoncontrols which file is used
The next lesson covers linting, formatting, and type checking in CI.