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
-
Solution
tsc --resolveJsonModule --esModuleInterop JSONReader.ts
- Repository: https://github.com/tekmi/prodding-typescript/tree/master/esm-default-import-json-file
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.
- ESM are not natively supported by all Web Browsers yet, nor by the NodeJS 12.x Engine (unless via --experimental-modules flag).
- Because ESM have been standardized by Ecma International under ECMA-262 JavaScript Language Specification, they should eventually become a preferred, go-to solution.
- Even NodeJS Engine, which has been favoring and using CommonJS modules for years, has finally brought ES Modules to NodeJS 13.2.0 Version in November 2019.
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.
- Both of them offer to import JSON files via ES Modules out of the box, so please read the Parcel Guide to JSON and Webpack Guide to Loaders to learn more.
- We won't go here into the details on how to set Webpack and Parcel, but please check the accompanying repository for quick examples: Reading JSON file with Parcel and Reading JSON file with Webpack.
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 withtsc --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"
.