What is ES module interoperability?
ES module interoperability in TypeScript refers to the ability to seamlessly import modules written in the CommonJS format (like many Node.js packages) using the ES module `import` syntax, treating them as if they had a default export, even when they don't explicitly define one.
Understanding ES Modules and CommonJS
Before modern JavaScript standardized ES Modules (import/export), Node.js primarily used CommonJS (require/module.exports). These two module systems have different conventions for exporting and importing values, particularly concerning default exports. CommonJS modules often export an object directly or a function, which ES modules would typically import as a named export or the entire module object.
The `esModuleInterop` Compiler Option
The esModuleInterop compiler option in TypeScript addresses this discrepancy. When set to true, TypeScript introduces 'synthetic default exports' for CommonJS modules. This allows you to use the more natural ES module default import syntax (import MyModule from 'my-commonjs-module';) even if the CommonJS module only exports named values or a single object via module.exports.
- Allows
import * as X from 'module'to correctly namespace all exports from a CommonJS module. - Enables
import X from 'module'to work for CommonJS modules that do not explicitly have adefaultexport. - Reduces the need for
import X = require('module')syntax, promoting a more consistent ES module style.
Under the hood, when esModuleInterop is true, TypeScript generates helper code in the compiled JavaScript output (often __importDefault and __importStar functions). These helpers inspect the imported module and, if it appears to be a CommonJS module without an explicit __esModule flag, they create a synthetic default export that points to the entire module.exports object.
Example
Consider a simple CommonJS module:
/* commonjs-module.js */
module.exports = {
a: 1,
b: 'hello'
};
Without esModuleInterop: true in your tsconfig.json:
/* consumer.ts */
// This will typically result in a TypeScript error:
// 'Module '"./commonjs-module"' has no default export.'
import MyModule from './commonjs-module';
// You might need to import using CommonJS syntax or a workaround:
// import MyModule = require('./commonjs-module');
// OR
// import * as MyModuleNamespace from './commonjs-module';
// const MyModule = MyModuleNamespace.default || MyModuleNamespace;
With esModuleInterop: true in your tsconfig.json:
/* consumer.ts */
import MyModule from './commonjs-module';
console.log(MyModule.a); // Output: 1
console.log(MyModule.b); // Output: 'hello'
// Named imports also work for CommonJS modules in many cases:
import { a } from './commonjs-module';
console.log(a); // Output: 1
Why is it important?
It greatly improves the developer experience when working in a mixed environment where you might have ES modules alongside CommonJS modules (e.g., a modern frontend project using legacy Node.js packages). It allows developers to write code using a consistent ES module import syntax, reducing confusion and simplifying module consumption across different module systems.
Configuration
To enable ES module interoperability, set esModuleInterop to true in your tsconfig.json:
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs", // Or "esnext", "node16", etc.
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}