Resolving module transpiling error between test and build code

Recently I faced a very odd problem while writing tests in TypeScript. While running the tests, an error was thrown that the module gray-matter (Github), which is a front matter parser. But when I build the code, the emitted JavaScript works perfectly.

This post is all about tracing the error of that import.

TLDR; Click here to read the TypeScript documentation on esModuleInterop option. Enabling this options also enables allowSyntheticDefaultImports option. Click here to read about it.

The following are two of multiple ways to import from a module in TypeScript -

  • Importing all exports of a module
import * as someModule from "some-module";
// Which transpile to the following
// const someModule = require("some-module");
  • Importing the default export
import someModule from "some-module";
// Which transpile to the following
// const someModule = require("some-module").default;

The difference between those two imports -

import * as someModule from "some-module";import someModule from "some-module";
Turns into a namespace importTurns into a default import
Can only be an object (ES6 module spec)Can be function, object, or others

By enabling esModuleInterop options, change the behavior of the compiler and adds helper functions that provide a shim to ensure compatibility in the emitted JavaScript. You can go to the TypeScript documentation to view detailed examples with emitted code.

The Problem

The Node.js package eco-system is huge. Also, there are multiple module formatting system, including but not limited to -

  • CommonJS (used by Node.js)
  • ES6
  • AMD
  • UMD etc.

Unlike ES6, most module formatting systems didn’t adhere to a specific spec. Thus when using a package that is older or uses another module formatting that doesn’t use the ES6 spec, will throw errors. In my case, I found it out with the package gray-matter.

First, I didn’t enable esModuleInterop. So, I used the import below -

import * as matter from "gray-matter";

which results in the following error when I was running test in Jest (Github)

TypeError: matter is not a function

For reference, the gray-matter exports in the following way -

/**
 * Expose `matter`
 */

matter.cache = {};
matter.clearCache = function () {
  matter.cache = {};
};
module.exports = matter;

So, let’s go down the rabbit hole with Jest. This is the Babel (Github) configuration to support test file in TypeScript

// babel.config.js
module.exports = {
  presets: [
    ["@babel/preset-env", { targets: { node: "current" } }],
    "@babel/preset-typescript",
  ],
};

Bable uses @babel/preset-typescript package to configure TypeScript support. Now, @babel/preset-typescript uses another package named @babel/plugin-transform-typescript to transpile from TypeScript to JavaScript.

In the description the package @babel/plugin-transform-typescript (documentation), it says the following

--esModuleInterop This is the default behavior of Babel when transpiling ECMAScript modules.

That means, the import that I was using handled differently while running the test. That is because the test code and the compiled code are emitted using different module resolution configurations. Thus import * as matter from "gray-matter"; will transpile differently with the esModuleInterop enabled in the test code, and not enable in the compilation.

The Solution

It’s quite easy, enable the esModuleInterop options in tsconfig.json

{
  "compilerOptions": {
    "esModuleInterop": true
  }
}

And convert namespace imports to default imports -

import matter from "gray-matter";

The Lesson

Every tool comes with a default configuration. It’s crucial to keep track of the configuration that they use and always keep them in sync.

NameLink
TypeScript Compiler Option: esModuleInteropDocumentation
TypeScript Compiler Option: allowSyntheticDefaultImportsDocumentation
gray-matterGithub
Front MatterWikipedia
JestGithub
BabelGithub
@babel/preset-typescriptDocumentation
@babel/plugin-transform-typescriptDocumentation