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.

6 min read

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 valueEmitted styleBest for
commonjsrequire() / module.exportsLegacy Node.js (pre-v12)
es2015 / es2020 / es2022 / esnextimport / export statementsBundled apps, modern browsers
nodenextESM or CJS based on package.jsonModern Node.js (v12+)
preserveImports left unchangedBundlers (Vite, Webpack, Next.js)
node16 / node18 / node20Like nodenext, pinned to Node versionSpecific 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:

typescripttypescript
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:

javascriptjavascript
"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:

typescripttypescript
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:

moduleDefault moduleResolution
commonjsnode10
nodenextnodenext
preservebundler
esnextclassic (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

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.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between module and moduleResolution?

module controls how import/export statements appear in the emitted JavaScript files. moduleResolution controls how TypeScript finds the files that those imports refer to. They are related but separate: you can use ESNext modules with node-style resolution, for instance.

Should I use CommonJS for new projects?

No. Use nodenext for Node.js projects and preserve or ESNext for bundled frontend projects. CommonJS is a legacy module system. Modern Node.js supports ESM natively, and bundlers handle ESM efficiently.

Why does the module option exist if bundlers strip types anyway?

TypeScript's type-checking and module resolution behavior depend on module. Even with --noEmit, setting the correct module value ensures TypeScript understands how your imports will behave at runtime, which affects the types of imported values.

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.