module
Sets the module format tsc emits: esnext, commonjs, node16/nodenext or preserve.
"module": "commonjs" | "es6" | "es2015" | "es2020" | "es2022" | "esnext" | "node16" | "node18" | "node20" | "nodenext" | "preserve" | "amd" | "umd" | "system" | "none"- Added in TypeScript 1.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$commonjsoptional | string | Emits require() and module.exports. The historic default when target was ES5. |
$esnextoptional | string | Emits ECMAScript import/export untouched. The usual choice when a bundler consumes the output. |
$es2015 / es6 / es2020 / es2022optional | string | ES module syntax pinned to a spec year. ES2020 adds dynamic import and import.meta; ES2022 adds top-level await. |
$node16 / node18 / node20optional | string | Node's dual module system, pinned to a Node major. Emits CommonJS or ESM per file based on the file extension and the nearest package.json "type". node18 (TS 5.8) adds import attributes; node20 adds require(ESM). |
$nodenextoptional | string | Same as node16/node18/node20 but tracks the newest Node behaviour. Implies target esnext. |
$preserveoptional | string | Added in TypeScript 5.4. Leaves ESM syntax as ESM and import x = require(...) as CommonJS; nothing is coerced into one format. Implies moduleResolution bundler. |
$amd / umd / system / noneoptional | string | Legacy browser-era loader formats. Deprecated in TypeScript 6.0. |
Return value
Determines the module syntax written into the emitted JavaScript, and the default for moduleResolution.
Errors
Errors if the chosen value conflicts with moduleResolution — for example moduleResolution "bundler" requires module "esnext" or "preserve", and moduleResolution "node16"/"nodenext" requires a matching node* module value.
Changelog
- PHP 4.7Added node16 and nodenext, which pick CommonJS or ESM emit per file from the extension and package.json "type".
- PHP 5.4Added preserve, which keeps each import and export in the format it was authored in.
- PHP 5.8Added node18 as a pinned replacement for node16 with import attributes support.
- PHP 6.0The default became esnext (it was commonjs when target was ES5). The amd, umd, systemjs and none values were deprecated.
module decides which module syntax tsc writes into the emitted JavaScript. It is not only an emit switch: because TypeScript checks your code against the format it would emit, this option also changes how specifiers resolve and how CommonJS packages are typed.
Choosing a value
- nodenext — or a pinned
node16/node18/node20— when Node.js runs the output directly. Emit is decided per file from the extension (.mts,.cts) and the nearestpackage.json"type", so one project can hold both formats at once. - esnext when a bundler consumes the output and should see untouched
import/export. - preserve when a tool reads your syntax as authored — Bun, tsx, or a bundler with
noEmit. Each statement keeps its own format. - commonjs only for older Node.js or tooling that cannot read ESM.
What trips people up
Setting module does not make Node.js treat your files as ESM. Node decides that from the file extension and "type": "module" in package.json. Emitting import statements into a file Node loads as CommonJS produces the familiar Cannot use import statement outside a module error at runtime, not at compile time.
Changing module also silently changes the default for moduleResolution, so a change here can alter which files your imports resolve to.
Example
// tsconfig.json
// { "compilerOptions": { "module": "nodenext", "target": "esnext" } }
// package.json
// { "type": "module" }
export function greet(who: string): string {
return `Hello, ${who}`;
}
// nodenext + "type": "module" -> emitted unchanged:
// export function greet(who) { ... }
// module: "commonjs" -> emitted as:
// exports.greet = greet;
console.log(greet("world"));
// -> Hello, worldWhen to use it
- Targeting Node.js directly with a mixed ESM/CommonJS package via nodenext.
- Handing untouched ESM to a bundler such as Vite, Rollup or esbuild with esnext.
- Publishing a library that must still be require()-able from older Node.js.
- Type-checking a Bun or tsx project with preserve and noEmit.
Frequently asked questions
What is the difference between module esnext and nodenext?
esnext always emits ES module syntax, while nodenext decides per file from the extension and package.json "type", so the same project can emit both ESM and CommonJS. Use esnext when a bundler consumes the output and nodenext when Node.js runs it directly.
Why do I get "Cannot use import statement outside a module" when module is esnext?
Because module only controls what tsc writes, not how Node.js loads it. Node treats a .js file as CommonJS unless the nearest package.json has "type": "module" or the file is named .mjs — add one of those, or switch to module nodenext.
Should I still use module commonjs?
Only for older Node.js versions or tooling that cannot load ESM. For new Node projects use nodenext, and for bundled code use esnext or preserve; TypeScript 6.0 made esnext the default.
More examples
One project, two emit formats under nodenext
Under nodenext the format is decided per file, not per project. config.mts emits ESM, legacy.cts emits require/module.exports, and main.ts follows package.json. The import must spell the emitted extension (.mjs) even though the source file is .mts.
// package.json: { "type": "module" }
// src/config.mts -> always ESM
export const port = 8080;
// src/legacy.cts -> always CommonJS
import config = require("./config-shim.cjs");
export = { port: config.port };
// src/main.ts -> ESM, because package.json says "type": "module"
import { port } from "./config.mjs"; // note: .mjs, not .mts
console.log(`listening on ${port}`);
// -> listening on 8080preserve keeps each statement in its authored form
preserve is the only value that refuses to convert between formats. ESM stays ESM and import = require stays CommonJS in the same file, which is what bundlers and Bun actually accept. It implies moduleResolution bundler and esModuleInterop for checking.
// tsconfig.json
// { "compilerOptions": { "module": "preserve", "noEmit": true } }
import { readable } from "./stream.js";
import legacy = require("./legacy.cjs");
export const size = 4;
export default readable;
console.log(typeof legacy, size);
// -> object 4
// Emitted output, if emit were on:
// import { readable } from "./stream.js"; // untouched
// const legacy = require("./legacy.cjs"); // untouched CommonJS
// export const size = 4;
// export default readable;
Discussion