Duck Who Codes's logo

esModuleInterop

The problem

So I was working on a TypeScript codebase where I needed to create a new package. As lazy as I could, I copy-pasted some files from another similar package. Everything was all smooth sailing until I ran the tests. Boom!

axios_retry_1.default is not a function

Noob-to-TS me asked: “OK, why is it calling default?”

A quick Google search told me that when TypeScript transpiles the code to JavaScript, a default export assigns the exported variable to the default property of module.exports.

export default Foo
// becomes
exports.default = Foo

Therefore, when we do a default import like this:

import axiosRetry from 'axios-retry'

We actually expect the default property to be callable. But axios-retry is written in plain JavaScript. It doesn’t export default.

Well, that’s not a big problem. I imported modules written in CommonJS to TypeScript before. A star import would always work.

import * as axiosRetry from 'axios-retry'

But this time, I was thinking if there was a better way to do this. I hate extra meaningless keystrokes in my code. And that’s how I stumbled across esModuleInterop.

What is esModuleInterop?

It’s an option in tsconfig.json that allows importing CommonJS modules in compliance with the ES6 spec. But what does the spec says? It says: a star import can only be a plain object, not callable. In this case, axiosRetry is not an object. It’s a function I actually want to call. So the star import here isn’t compliant with the spec.

By setting esModuleInterop to true, I can import axios-retry by the default import syntax and also get rid of the error I met earlier. Nice and clean!

import axiosRetry from 'axios-retry' // spec compliant

axiosRetry(axios) // can call it too!

If I try calling axiosRetry when it’s star imported, I’ll get a compile-time error.

import * as axiosRetry from 'axios-retry'

axiosRetry(axios) // -> This expression is not callable.

(Optional) How does it work?

When enabling esModuleInterop, modules are not imported directly but instead through a helper function __importDefault.

const axios_retry_1 = __importDefault(require('axios-retry'))

The __importDefault function looks like this:

var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};

As you can see, if the imported module is not an ES6 module (checked via the __esModule property), this function assigns the module to the default property and returns it. Otherwise, it returns an untouched module.