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 enablesallowSyntheticDefaultImports
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 import | Turns 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.
Reference Links
Name | Link |
---|---|
TypeScript Compiler Option: esModuleInterop | Documentation |
TypeScript Compiler Option: allowSyntheticDefaultImports | Documentation |
gray-matter | Github |
Front Matter | Wikipedia |
Jest | Github |
Babel | Github |
@babel/preset-typescript | Documentation |
@babel/plugin-transform-typescript | Documentation |