esModuleInterop

Emits interop helpers so default and namespace imports of CommonJS modules work.

Signature
"esModuleInterop": boolean
  • Added in TypeScript 2.7
  • Deprecated since PHP 6.0 (only the false setting; the flag itself remains)

Parameters

ParameterTypeDescription
$trueoptionalbooleanEmits the __importDefault and __importStar helpers, wraps every require() of an imported module in them, and turns on allowSyntheticDefaultImports. Namespace imports become real namespace objects that cannot be called.
$falseoptionalbooleanLegacy behaviour: import * as x is compiled to a bare require, so a callable CommonJS export stays callable, and a default import compiles to require(...).default. Deprecated in TypeScript 6.0.

Return value

Changes both type checking and emit so ES module import forms behave correctly against CommonJS modules.

Errors

With the flag off, `import moment from "moment"` on a CommonJS module is error TS1259 ("can only be default-imported using the esModuleInterop flag"). With it on, calling a namespace import is error TS2349, since ES namespace objects are not callable.

Changelog

  • PHP 2.7Introduced, along with the __importDefault and __importStar emit helpers.
  • PHP 4.7Defaults to true when module is node16 or nodenext (and later preserve), rather than being off unless asked for.
  • PHP 6.0Can no longer be set to false; the safer interop behaviour is always enabled. Code relying on `import * as express from "express"` being callable must switch to a default import.

esModuleInterop repairs two assumptions TypeScript made before 2.7, both of which diverged from what Node.js, Babel and bundlers actually do.

The two fixes

First, import * as moment from "moment" used to compile to a bare require, so moment() was callable — convenient, but illegal under the ES module spec, where a namespace object is a frozen record of exports. Second, import moment from "moment" compiled to require("moment").default, which is undefined for the many CommonJS packages that assign to module.exports directly.

How the emit changes

Turning it on injects two helpers. __importDefault checks the __esModule marker and, when it is absent, wraps the whole module as { "default": mod } — so a default import lands on the real module.exports. __importStar copies the module's own properties onto a fresh object and sets default on it, producing a spec-shaped namespace. This is exactly what Babel emits, which is why the two toolchains finally agree.

What trips people up

It is an emit change, so every consumer of your compiled output inherits it. And the fix cuts both ways: once it is on, a namespace import is no longer callable, so old import * as express from "express"; express() code must become a default import.

Example

Example · typescript
// tsconfig.json
//   { "compilerOptions": { "module": "commonjs", "esModuleInterop": true } }

// "lodash/chunk" is CommonJS: module.exports = chunk, with no `default`.
import chunk from "lodash/chunk";
import * as path from "node:path";

// Emitted:
//   const chunk_1 = __importDefault(require("lodash/chunk"));
//   const path = __importStar(require("node:path"));
//   chunk_1.default([...])

console.log(chunk(["a", "b", "c", "d"], 2));
console.log(path.join("src", "index.ts"));
// -> [ [ 'a', 'b' ], [ 'c', 'd' ] ]
// -> src/index.ts

When to use it

  • Importing a CommonJS package such as express or lodash with a default import.
  • Aligning tsc output with Babel or esbuild so both toolchains agree on imports.
  • Fixing "TypeError: x is not a function" after switching a project to ES modules.
  • Consuming Node built-ins with import * as path while staying spec-correct.

Frequently asked questions

What does esModuleInterop actually do?

It emits the __importDefault and __importStar helpers so default imports of CommonJS modules resolve to module.exports and namespace imports become spec-correct namespace objects. It changes emitted JavaScript, and it also turns on allowSyntheticDefaultImports for type checking.

What is the difference between esModuleInterop and allowSyntheticDefaultImports?

esModuleInterop changes both type checking and the emitted JavaScript, while allowSyntheticDefaultImports only relaxes type checking and emits nothing. Enabling esModuleInterop implies allowSyntheticDefaultImports; the reverse is not true.

Why did my import * as x stop being callable?

Because esModuleInterop makes namespace imports produce a real ES namespace object, which is never callable, and that is error TS2349. Change it to a default import, such as import express from "express".

More examples

The helper tsc injects

The helper is a two-line branch on the __esModule marker. A real transpiled ES module is handed back untouched so its own default survives; a plain CommonJS module gets wrapped so module.exports becomes the default. Set importHelpers to import these from tslib instead of inlining them in every file.

Example · typescript
// This is emitted at the top of the output file, not written by you:
var __importDefault = (this && this.__importDefault) || function (mod) {
  return (mod && mod.__esModule) ? mod : { "default": mod };
};

// A CommonJS module with no __esModule marker:
const commonjsModule = { chunk: (xs: unknown[]) => xs.length };

// An ES module transpiled to CommonJS, which does carry the marker:
const esModule = { __esModule: true, default: () => "real default" };

console.log(__importDefault(commonjsModule).default === commonjsModule);
console.log(__importDefault(esModule).default());
// -> true
// -> real default

What breaks when the flag is turned on

This is the migration every project hits, and TypeScript 6.0 forces it by refusing esModuleInterop: false. A namespace object is a record of exports and is never callable, so any factory-style CommonJS package must be default-imported.

Example · typescript
// Before (esModuleInterop: false) — compiled to a bare require, so callable:
//   import * as express from "express";
//   const app = express();          // worked, but is not valid ESM

// After (esModuleInterop: true) — express is a namespace object:
//   const app = express();
//   error TS2349: This expression is not callable.

// The fix is a default import:
import express from "express";
const app = express();
app.get("/health", (_req, res) => res.send("ok"));

console.log(typeof express, typeof app.get);
// -> function function

Discussion

  • Be the first to comment on this lesson.