Module Option in tsconfig
The module option controls how TypeScript emits import and export statements. Learn the difference between CommonJS, ESNext, nodenext, and preserve, and how to pick the right one.
The module option in tsconfig controls how TypeScript converts your import and export statements into JavaScript. It determines whether your output files use require() and module.exports (CommonJS) or import and export (ES modules), or whether the imports pass through unchanged for a bundler to handle.
This one setting affects module resolution, the types of imported values, and whether your code runs correctly in its target environment. Choosing the wrong value causes confusing import errors at runtime even when the TypeScript compilation succeeds.
The module values at a glance
| module value | Emitted style | Best for |
|---|---|---|
| commonjs | require() / module.exports | Legacy Node.js (pre-v12) |
| es2015 / es2020 / es2022 / esnext | import / export statements | Bundled apps, modern browsers |
| nodenext | ESM or CJS based on package.json | Modern Node.js (v12+) |
| preserve | Imports left unchanged | Bundlers (Vite, Webpack, Next.js) |
| node16 / node18 / node20 | Like nodenext, pinned to Node version | Specific Node.js versions |
For any new project, you only need to know three of these: nodenext, preserve, and esnext. The rest exist for legacy compatibility or specific Node.js version targeting.
CommonJS: the legacy default
CommonJS was the default module system for Node.js from its earliest days. When module is set to commonjs, import statements become require() calls and export statements become assignments to module.exports:
import { add } from "./math";
export const result = add(2, 3);With module set to commonjs, tsc rewrites both the import and the export statement using Node.js's original module format. Here is the emitted output:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const math_1 = require("./math");
exports.result = (0, math_1.add)(2, 3);The import becomes a require() call, and export becomes a property assignment on exports. This format works in Node.js but not in browsers without a bundler. It does not support tree-shaking, and it makes static analysis harder for tools.
Do not use CommonJS for new projects. It exists for backward compatibility with older Node.js codebases.
ESNext and the ES20xx family
Setting module to esnext, es2020, or es2022 tells TypeScript to emit standard ECMAScript import and export syntax. The source code passes through largely unchanged:
import { add } from "./math";
export const result = add(2, 3);The output is nearly identical. ES2020 and above add support for dynamic imports (import()) and import.meta. ES2022 adds support for top-level await.
These settings are ideal for projects that use a bundler like Vite, Webpack, or esbuild. The bundler reads the ESM syntax and produces optimized bundles for the browser. TypeScript does not need to transform the module syntax because the bundler handles that step.
nodenext: the modern Node.js choice
The nodenext value is the recommended setting for Node.js projects. Unlike the simpler ESNext setting, nodenext understands Node.js's dual module system and emits the correct format based on your package.json.
If your package.json has "type": "module" or your file has a .mts extension, TypeScript emits ESM-style imports and exports. If your package.json has "type": "commonjs" (or no type field) and your file has a .cts extension, it emits CommonJS require() calls.
This lets a single project mix ESM and CommonJS files naturally, matching exactly how Node.js would interpret each file at runtime. Nodenext also enforces that relative ESM imports include file extensions, which is required by the Node.js ESM implementation.
preserve: hands-off for bundlers
The preserve value, added in TypeScript 5.4, tells the compiler to leave import and export statements exactly as written. If you wrote an ESM import, it stays an ESM import. If you wrote a CommonJS require() call, it stays a require() call.
This is the best choice for projects where a bundler handles module transformation. The compiler does not make assumptions about what the bundler will do. It simply checks types and passes the module syntax through untouched.
When module is preserve, moduleResolution defaults to bundler, which understands package.json exports and does not require file extensions on relative imports. This combination is ideal for Next.js, Vite, and similar frameworks.
How module affects moduleResolution
Setting module also sets a default for moduleResolution, which controls how TypeScript locates the files your imports refer to:
| module | Default moduleResolution |
|---|---|
| commonjs | node10 |
| nodenext | nodenext |
| preserve | bundler |
| esnext | classic (override this to bundler) |
You almost never need to set moduleResolution manually. Pick the right module value for your runtime, and the resolution strategy follows. If you use esnext with a bundler, set moduleResolution to bundler explicitly. The classic default for esnext is not what you want.
Choosing the right value
Start with these questions:
Is your code running in Node.js? Use nodenext. It produces the correct output for your package.json type field and enforces Node.js-compatible import paths.
Is your code a frontend app bundled by Vite, Next.js, or Webpack? Use preserve. Your bundler handles module transformation, and TypeScript stays out of the way.
Is your code a library published to npm? Use nodenext with declarations enabled. Consumers using both ESM and CommonJS can import your package correctly.
For more on how the compiler processes your configuration, see the TypeScript compiler explained. For the bigger picture of all important options, read about important tsconfig options explained.
Rune AI
Key Insights
- module controls the module system in emitted JavaScript output.
- Use nodenext for modern Node.js; it supports both ESM and CJS based on package.json.
- Use preserve for bundled apps; imports pass through unchanged for the bundler to handle.
- CommonJS is legacy; use it only for old Node.js projects.
- moduleResolution defaults based on module, so choose module first.
Frequently Asked Questions
What is the difference between module and moduleResolution?
Should I use CommonJS for new projects?
Why does the module option exist if bundlers strip types anyway?
Conclusion
The module option determines how your imports and exports appear in the JavaScript output. For new Node.js projects, use nodenext. For bundled frontend projects, use preserve or ESNext. Avoid CommonJS for new code. The right choice ensures your emitted code matches what your runtime or bundler expects.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.