JavaScript module import hoisting
At my current workplace, I work as a security engineer. My day-to-day tasks include submitting security patches to the software engineering teams. Teams write and develop microservices using a myriad of programming languages.
While working on one of our TypeScript projects, I came across the concept of import hoisting and wondered why it did not work as officially documented. That drove me curious, so I started trying different ways to import modules in TypeScript, which eventually led to an Eureka moment.
In this post, I want to share my experience and journey of discovery.
The tooling
Create a new empty directory, and install the necessary tooling:
mkdir testing && cd testing
npm init
npm install ts-node tsc typescript
For my setup, these are the runtime and library versions:
node v12.18.3
npm v6.14.6
ts-node v9.0.0
tsc v1.20150623.0
typescript v4.0.2
Does TypeScript hoist import?
For Node.js to detect a file as a module, each should have at least one import
or export
statement. Create these three files accordingly:
// index.ts
console.log(A);
console.log(B);
console.log('c');
import {A} from './a';
import {B] from './b';
// a.ts
console.log('a');
export const A = 'export A';
// b.ts
console.log('b');
export const B = 'export B';
Execute the main file:
npx ts-node main.ts
It prints out an error:
Cannot read property 'A' of undefined
What?
Shouldn’t the import
statement be hoisted to the top of the file in index.ts
?
So I ran tsc
to transpile the file to JavaScript and inspect what executes:
npx tsc index.ts
Attempting to execute the transpilation result:
node index.js
Yields the same error as executing ts-node
:
TypeError: Cannot read property 'A' of undefined
Inspecting index.js
:
// index.js, transpiled from index.ts using tsc
"use strict";
exports.__esModule = true;
console.log(a_1.A);
console.log(b_1.B);
console.log('c');
var a_1 = require("./a");
var b_1 = require("./b");
Here we see tsc
merely converts the import
statement into var ... = require(...)
. JavaScript only hoists var
declarations and does not execute the corresponding require()
function yet. Essentially this is how it looks like after hoisting:
// index.js, after hoisting
"use strict";
var a_1;
var b_1;
exports.__esModule = true;
console.log(a_1.A);
console.log(b_1.B);
console.log('c');
require("./a");
require("./b");
After declaration, both a_1
and b_1
are still undefined
. That is why we get the error Cannot read property 'A' of undefined
.
So at this point, TypeScript does not compile to ES6 modules by default.
But does the native ES6 module hoist import?
Similarly to the experiment in TypeScript, we create three ES6 modules, adapting from the files in the previous trial:
// index.mjs
console.log(A);
console.log(B);
console.log('c');
import {A} from './a.mjs';
import {B] from './b.mjs';
Re-use a
and b
modules:
cp a.ts a.mjs
cp b.ts b.mjs
Run them:
node index.mjs
Result:
(node:2210) ExperimentalWarning: The ESM module loader is experimental.
a
b
export A
export B
c
It works! JavaScript hoists ES6 modules. It executes the two import
statements first, containing the console.log
from the imported modules, printing “a” and “b” respectively. Then, it executes the rest of the console.log
in the index file, printing the constant value A
and B
, and finally the literal string “c”.
Wrapping up
So yes, ES6 modules are indeed hoisted. For TypeScript, by default, it does not seem to transpile the import
statement to native ES6 import, but maybe there is an option flag to trigger it to do so …