Importing JSON file using ES Modules

|6 min read|
Adam
Adam


Explore Other Articles from This Series

To prod

I wanted to import a JSON file using ES Modules default import syntax inside my TypeScript application, but...

Prerequisites

  • NodeJS Long Time Support (Version 12.x)
  • NodeJS Package Manager (Version 6.x)
  • TypeScript (Version 3.9.x)
  • The settings.json file with the valid JSON content:

    {
      "env": "production"
    }

TL;DR

Life without TypeScript

ES Modules (ESM), next to other module systems (i.e.: AMD, UMD, and CommonJS), are yet another way of bringing modularity to JavaScript.

Although we can already declare script as module in modern browsers, we still cannot forget about the almighty module bundlers that allowed bundling JavaScript modules into a single JavaScript file from the very beginning. Nowadays Webpack and Parcel are dominating the space.

NodeJS, on the other hand, has embraced modularity from its beginning thanks to the CommonJS syntax. Nevertheless, being able to eventually replace CommonJS with ES Modules and work with one module system only is fascinating.

With that, let's check together whether TypeScript has any tricks up its sleeve to handle ES Modules more gracefully.

Embracing TypeScript

Generally speaking, TypeScript is mostly concerned about typechecking, leaving the topics of modularity and bundling to other tools like Webpack, Packer, SystemJS, or AMD. However, by treating any file with a top-level import or export as a module, it usually does not stand in our way when we try to use ES Modules syntax.

Furthermore, depending on the combination of --target, --module, and --moduleResolution flags, TypeScript Compiler knows how to generate appropriate code for CommonJS, AMD, UMD, SystemJS, or ES Modules systems.

Is it going to protest when we try to import a JSON file using ES Modules default import syntax or let it go? Let's verify it by creating a simple file JSONReader.ts with the following content.

import settings from "./settings.json";

console.log(settings.env);

As soon as we compile it with tsc JSONReader.ts, we will instantly see the following error:

JSONReader.ts:1:22 - error TS2732: Cannot find module './settings.json'. Consider using '--resolveJsonModule' to import module with '.json' extension

Because TypeScript Compiler does not tolerate importing JSON files like that out of the box, let's try to get to the bottom of this issue by exploring the possible solutions together.

Solution Area

By shouting with the TS2732 error code, the TypeScript Compiler also gave us a decent hint, suggesting to use the --resolveJsonModule compiler flag.

tsc --resolveJsonModule JSONReader.ts

Unfortunately, this advice was not enough. We have immediately come across another error, as shown below.

error TS1259: Module '"~/prodding-typescript/esm-default-import-json-file/settings"' can only be default-imported using the 'esModuleInterop' flag

By default, even if the TypeScript Compiler reports a compilation error, it can still emit corresponding JavaScript files (in our case, this is JSONReader.js file). If we try to run it via NodeJS Interpreter like node JSONReader.js, we will get the following error:

~/prodding-typescript/esm-default-import-json-file/JSONReader.js:4
console.log(settings_json_1["default"].env);
                                       ^

TypeError: Cannot read property 'env' of undefined

By looking into the content of the JSONReader.js file (presented below), we see that TypeScript Compiler not only has converted ES Modules syntax to CommonJS, but also created an additional default namespace which has caused the above TypeError.

"use strict";
exports.__esModule = true;
var settings_json_1 = require("./settings.json");
console.log(settings_json_1["default"].env);

To fix it, let's follow yet another advice from TypeScript Compiler, this time regarding the --esModuleInterop compiler flag.

tsc --resolveJsonModule --esModuleInterop JSONReader.ts

Or to be even more specific, because --esModuleInterop and --allowSyntheticDefaultImports are closely related to each other, we could run it like so:

tsc --resolveJsonModule --esModuleInterop --allowSyntheticDefaultImports true JSONReader.ts 

After that, all the TypeScript Compiler's errors are gone. If we run the re-generated JavaScript file with node JSONReader.js now, we should see the correct production text in the console.

When we examine this file again (presented below), we should spot a new __importDefault helper function that enables interoperability between CommonJS and ES Modules via the creation of namespace objects for all imports.

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
exports.__esModule = true;
var settings_json_1 = __importDefault(require("./settings.json"));
console.log(settings_json_1["default"].env);

That's all there is to it. Before we wrap it up, let's clarify a few things:

  • --resolveJsonModule has been introduced in TypeScript 2.9 and is false by default. This requires the developers to opt-in for this behavior, because loading the JSON files that way may consume lots of memory (underneath the require directive is used).
  • --esModuleInterop has been introduced in TypeScript 2.7 and is also false by default. If you set the --esModuleInterop to true, --allowSyntheticDefaultImports flag will be automatically set to true as well. (In this tutorial we didn't use tsconfig.json yet, but if you were about to create one with tsc --init, the esModuleInterop option will be set to true there)
  • TypeScript Compiler by default uses ES3 for the --target flag. This automatically sets the --module to CommonJS, which in turn sets the --moduleResolution to node. All these sane defaults allow us to use the --resolveJsonModule flag, which otherwise would not be possible. Feel free to set the --module or --target to ES6 and verify yourself if you get a TS5070 error code.
  • As for the bonus, we did not need to use the --esModuleInterop flag. There is an alternative syntax that imports the entire module into a single variable as follows: import * as settings from "./settings.json".