Question
I was looking at someone else's tsconfig.json file and noticed these options:
{
"compilerOptions": {
"moduleResolution": "node",
"target": "es6",
"module": "commonjs",
"lib": ["esnext"],
"strict": true,
"sourceMap": true,
"declaration": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"declarationDir": "./dist",
"outDir": "./dist",
"typeRoots": ["node_modules/@types"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
My main question is: what do these two settings mean?
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
I understand that they are related in some way to:
"module": "commonjs"
Can someone explain them in simple, practical language?
The official documentation for allowSyntheticDefaultImports says:
Allow default imports from modules with no default export. This does not affect code emit, just typechecking.
What exactly does that mean? If a module does not have an export default, when would a default import even make sense?
I also found this description of esModuleInterop difficult to understand:
Emit
__importStarand__importDefaulthelpers for runtime Babel ecosystem compatibility and enable--allowSyntheticDefaultImportsfor type system compatibility.
I would like a beginner-friendly explanation of what these options actually do, why they exist, and when to use them.
Short Answer
By the end of this page, you will understand why TypeScript sometimes struggles with imports from CommonJS modules, what allowSyntheticDefaultImports changes in type checking, what esModuleInterop changes in emitted JavaScript, and why these options are commonly enabled together in real projects.
Concept
TypeScript sits between your source code and the JavaScript module system you are targeting. The confusion here comes from the fact that there are two different module styles in the JavaScript ecosystem:
- ES Modules (ESM)
- Use
exportandimport - Example:
- Use
export default function greet() {
console.log("Hello")
}
import greet from "./greet"
- CommonJS (CJS)
- Use
module.exportsandrequire - Example:
- Use
module.exports = function greet() {
console.log("Hello")
}
Mental Model
Think of ES Modules and CommonJS as two different plug shapes.
- ES Modules are one plug type
- CommonJS modules are another plug type
Your TypeScript code wants to use a modern ES module-style plug:
import thing from "package"
But the actual package might be exporting itself in a CommonJS shape.
allowSyntheticDefaultImports
This is like saying:
"It's okay to pretend this plug fits while we are checking the blueprint."
It makes the code look acceptable to TypeScript.
esModuleInterop
This is like adding a real adapter so the plug actually fits when the code runs.
So a simple way to remember it is:
allowSyntheticDefaultImports= "let me write it"esModuleInterop= "make it work more reliably"
That is why esModuleInterop is usually the better setting when you are importing CommonJS packages using default-import syntax.
Syntax and Examples
Core idea
Here is a CommonJS module:
// math.js
module.exports = function add(a, b) {
return a + b
}
CommonJS usage
const add = require("./math")
console.log(add(2, 3))
ES module-style usage in TypeScript
import add from "./math"
console.log(add(2, 3))
Whether that TypeScript import is allowed and works correctly depends on your compiler settings.
Example 1: allowSyntheticDefaultImports
{
"compilerOptions":
Step by Step Execution
Consider this TypeScript code:
import path from "path"
const result = path.join("src", "index.ts")
console.log(result)
Assume module is set to commonjs.
What happens conceptually with esModuleInterop: true
Step 1: You write a default import
import path from "path"
You are saying: "Give me the module's default export and call it path."
Step 2: TypeScript checks the module
The path module is not written as a normal ES module with export default.
Without compatibility settings, TypeScript may complain because the module shape does not match a true default export.
Step 3: esModuleInterop allows compatibility
TypeScript treats the CommonJS module in a way that supports this style of import.
Real World Use Cases
Importing popular npm packages
Many packages in the Node.js ecosystem were historically published as CommonJS modules.
Examples developers often import with esModuleInterop enabled:
import express from "express"
import cors from "cors"
import dotenv from "dotenv"
This style is shorter and more familiar than older interop patterns.
Working in Node.js backends
In backend TypeScript apps, especially older CommonJS-based projects, these settings make it easier to use packages from npm without constantly switching import styles.
Migrating JavaScript projects to TypeScript
If a codebase already uses Babel or default-import syntax everywhere, enabling esModuleInterop makes migration smoother.
Using type definitions from DefinitelyTyped
Some type definition packages were written with compatibility assumptions in mind. These flags help TypeScript work more smoothly with those packages.
Consistent developer experience
Teams often choose esModuleInterop: true so developers can use one predictable import style across many dependencies, even if those dependencies were authored in different module formats.
Real Codebase Usage
In real projects, developers usually do not think about these flags every day. They just want imports to be clear and reliable.
Common pattern: enable esModuleInterop
A very common setup is:
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"strict": true
}
}
Because esModuleInterop also enables allowSyntheticDefaultImports, teams usually set only the first one.
Why teams prefer it
- cleaner imports
- less confusion when using npm packages
- better compatibility with examples from documentation and tutorials
- easier migration from Babel-based JavaScript projects
In library code vs application code
- Application code often enables
esModuleInteropfor convenience. - Library code may be more careful, especially if the library must support multiple module systems and produce clean public typings.
Common Mistakes
1. Thinking both options do the same thing
They are related, but not identical.
allowSyntheticDefaultImportsaffects type checking onlyesModuleInteropaffects runtime emit behavior and also enablesallowSyntheticDefaultImports
2. Assuming allowSyntheticDefaultImports makes runtime code work
Broken expectation:
import somePackage from "some-package"
If only allowSyntheticDefaultImports is enabled, TypeScript may accept the code, but the emitted JavaScript may still not behave correctly at runtime.
How to avoid it:
- Prefer
esModuleInteropwhen using default imports from CommonJS modules
3. Confusing default export with "main thing exported"
In CommonJS, this is valid:
module.exports = function () {
console.()
}
Comparisons
| Option / Concept | What it changes | Affects emitted JavaScript? | Typical use |
|---|---|---|---|
allowSyntheticDefaultImports | Lets TypeScript accept default imports from modules without a real default export | No | Type-checking convenience |
esModuleInterop | Adds compatibility helpers and enables synthetic default imports | Yes | Practical interoperability with CommonJS |
import * as x from "pkg" | Imports the module namespace/object | No special interop needed | Safer when you want the full module object |
import x from "pkg" | Imports the default export | Depends on module shape and config | Cleaner syntax when interop is enabled |
vs
Cheat Sheet
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true
}
}
Quick rules
-
allowSyntheticDefaultImports: true- allows default-import syntax during type checking
- does not change emitted JavaScript
-
esModuleInterop: true- changes emitted JavaScript for better CommonJS interop
- automatically enables
allowSyntheticDefaultImports
Easy memory trick
allowSyntheticDefaultImports= allow the syntaxesModuleInterop= allow the syntax and help it work at runtime
Common examples
With interop
express
FAQ
What does esModuleInterop do in TypeScript?
It helps TypeScript work more smoothly with CommonJS modules when you use ES module import syntax such as import x from "pkg".
Does esModuleInterop automatically enable allowSyntheticDefaultImports?
Yes. Enabling esModuleInterop also enables allowSyntheticDefaultImports.
What does "does not affect code emit, just typechecking" mean?
It means TypeScript will allow the import syntax during compile-time checks, but it will not change the generated JavaScript to make that syntax work at runtime.
Should I enable both options?
Usually you only need esModuleInterop: true, because it already includes the behavior of allowSyntheticDefaultImports.
Why do some examples use import * as express from "express"?
That style was commonly used when interop support was stricter or disabled. With esModuleInterop, many developers now use import express from "express" instead.
Is CommonJS the same as export default?
No. is not exactly the same thing as an ES module , even though they can feel similar in everyday use.
Mini Project
Description
Build a small TypeScript project that imports a CommonJS-style module in two different ways so you can see why esModuleInterop exists. This project demonstrates the difference between type-checking convenience and runtime compatibility.
Goal
Create a TypeScript app that imports a CommonJS export using default-import syntax and understand why esModuleInterop makes that pattern easier and safer.
Requirements
- Create a CommonJS-style module that exports one function with
module.exports. - Import that module into a TypeScript file using default-import syntax.
- Configure TypeScript with
module: "commonjs"andesModuleInterop: true. - Run the compiled code and verify that the imported function works.
- Compare the setup mentally with what would happen if interop were disabled.
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.
Angular @ViewChild Returning Undefined: Lifecycle, Child Components, and Fixes
Learn why Angular @ViewChild can be undefined, when it becomes available, and how to access child components correctly using lifecycle hooks.