esModuleInterop in tsconfig: A Complete Guide for TypeScript

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why esModuleInterop Is More Than Just a Checkbox in tsconfig

\n\n

If you've ever written TypeScript and imported a library like express or lodash, you've likely encountered a moment when the compiler complains about import * as express from 'express'. Or, conversely, you calmly use import express from 'express', but in the generated JavaScript code you see a strange construct module.exports.default. What kind of magic is this? The magic (or lack thereof) is the esModuleInterop flag in your tsconfig.json.

\n\n

This parameter is one of the most popular questions on Stack Overflow (thousands of views!), and it's no wonder: without understanding it, it's easy to fall into the trap of module incompatibility. In this tutorial, we won't just explain the theory; we'll walk through the journey from problem to solution with live examples. Let's go!

\n\n

The Problem: Conflict of Two Module Worlds

\n\n

TypeScript is a superset of JavaScript, and JavaScript historically has two main module systems:

\n
    \n
  • CommonJS (CJS) — the Node.js standard, uses require() and module.exports. Example: const express = require('express');
  • \n
  • ES Modules (ESM) — the modern standard, uses import and export. Example: import express from 'express';
  • \n
\n

The problem is that these systems handle default exports (default export) differently. In ESM, a module can have one export default, and it is imported as import name from 'module'. In CommonJS, there is no concept of a "default export" — there is only the module.exports object, which can be anything.

\n\n

When TypeScript compiles your code to CommonJS (which is often needed for Node.js), it must decide: how to represent import x from 'y' as require()? Without esModuleInterop, TypeScript does this literally: it creates module.exports.default, which breaks the import of libraries written for CommonJS.

\n\n

What is esModuleInterop?

\n\n

esModuleInterop is a flag in tsconfig.json that enables additional compatibility between CommonJS and ES Modules. When set to true, TypeScript generates helper code that allows you to write import foo from 'bar' even for CommonJS modules that don't have a default export.

\n\n

By default in TypeScript 4.x and above, this flag is true (if you use --init), but in older projects it was often turned off. Why? Because it changes the behavior of imports, and without understanding it, this leads to bugs.

\n\n

How It Looks in tsconfig.json

\n
{\n  "compilerOptions": {\n    "esModuleInterop": true,\n    // other settings...\n  }\n}\n
\n\n

Enabling this flag also automatically enables allowSyntheticDefaultImports, which allows default imports from modules without a real default export at the type level.

\n\n

Step-by-Step Tutorial: From Problem to Solution

\n\n

Let's break down the situation with a real example. Suppose we have a simple library in CommonJS:

\n\n
// myLib.js (CommonJS)\nmodule.exports = function greet(name) {\n  return `Hello, ${name}!`;\n};\n
\n\n

We want to use it in a TypeScript project. Let's create a file index.ts:

\n\n
// index.ts\nimport greet from './myLib';\nconsole.log(greet('World'));\n
\n\n

Now let's see what happens during compilation with different esModuleInterop settings.

\n\n

Step 1: Without esModuleInterop (false)

\n\n

Let's run the compilation with "esModuleInterop": false. TypeScript will throw an error:

\n
Module '"myLib"' has no d                

Blogs

Book Recommendations