CommonJS exports one value (module.exports, often a function). An ES module exports named bindings plus an optional default. Every tool bridges the two differently, and the failures come from tsc, your bundler, or Node disagreeing.
What the flags do.
esModuleInteropaffects emit and types. Formodule: commonjsoutput,import x from "cjs"becomes__importDefault(require("cjs")).defaultandimport * as nsgoes through__importStar. The helpers check the__esModulemarker that transpiled ESM carries. Types follow: a default import of anexport =module is allowed.allowSyntheticDefaultImportschanges only type checking, so a default import of a module without one is permitted. Emit is unchanged, which is why the TypeScript handbook says to avoid it withoutesModuleInterop.
Version note. TS 6.0 deprecated setting either flag to false; on TS 7.0.2 --esModuleInterop false is error TS5108.
Native Node ESM differs from the helpers. Per the Node docs, import x from "./c.cjs" always gives module.exports. Named imports come from static analysis, so a name assigned dynamically is missing. Node does not look at __esModule, so a transpiled module gets a "double default" (x.default).
Practical rules.
- Default-import CommonJS modules that use
export =/module.exports = .... - Do not write
import * as fn from "cjs"and then callfn(). - In Node-targeted code use
module: nodenext, which checks per-file formats;export =is an error in ESM files (TS1203). - A
.d.tsfile is ESM or CJS by the nearestpackage.json"type", while.d.mts/.d.ctsare fixed (handbook theory page). Dual packages ship the right one perexportscondition.