ES Modules in Depth

Modules in JavaScript are much more straightforward since ES Modules were added to the specification. Modules are separated by file and loaded asynchronously. Exports are defined using the export keyword; values can be imported with the import keyword.

While the basics of importing and exporting individual values is pretty easy to grasp and use, there are many other ways to work with ES Modules to make your imports and exports work the way you need them to. In this lesson, we’ll go over all of the ways you can export and import within your modules.

One thing to remember is that exports and static imports can only happen at the top level of the module. You cannot export or statically import from within a function, if statement, or any other block. Dynamic imports, on the other hand, can be done from within a function; we’ll talk about those at the end of the lesson.


Default Export

Every module has a single “default” export, which represents the main value which is exported from the module. There might be more things exported, but the default export is what defines the module. You can only have one default export in a module.

const fruitBasket = new FruitBasket(); export default fruitBasket;

Notice that I have to first define the value before adding it to my default export. If I wanted to, I could export my value immediately, without assigning it to a variable. But I cannot assign it to a variable at the same time as exporting it.

We can export a function declaration and a class declaration by default without first assigning it to a variable.

export default function addToFruitBasket(fruit) { }

We can even export literal values as the default export.

Named Export

Any variable declaration can be exported when it is created. This creates a “Named Export” using the variable name as the export name.

export const fruitBasket = new FruitBasket();

We can also immediately export function and class declarations.

export function addToFruitBasket(fruit) { }
export class FruitBasket { }

If we wanted to export a variable which was already defined, we could do that by wrapping the variable in curly brackets around our variable name.

const fruitBasket = new FruitBasket(); export { fruitBasket };

We can even use the as keyword to rename our export to be different from the variable name. We can export other variables at the same time, if we wanted.

const fruitBasket = new FruitBasket();
class Apple {} export { fruitBasket as basketOfFruit, Apple };

Aggregate Exports

One thing that is common is importing modules from one module and then immediately exporting those values. It looks something like this.

import fruitBasket from "./fruitBasket.js"; export { fruitBasket };

This can get tedious when you are importing and exporting lots of things at the same time. ES Modules allows us to import and export multiple values at the same time.

export * from "./fruitBasket.js";

This will take all of the named exports of ./fruitBasket.js and re-export them. It won’t re-export default exports though, since a module can only have one default export. If we were to import and export multiple modules with default exports, which value would become the default export for the exporting module?

We can specifically export default modules from other files, or name the default export when we re-export it.

export { default } from "./fruitBasket.js"; export { default as fruitBasket } from "./fruitBasket.js";

We can selectively export different items from another module as well, instead of re-exporting everything. We use curly brackets in this case as well.

export { fruitBasket as basketOfFruit, Apple } from "./fruitBasket.js";

Finally, we can wrap up an entire module into a single named export using the as keyword. Suppose we have the following file.

export class Apple {}
export class Banana {}

We can now pack this into a single export which is an object containing all of the named and default exports.

export * as fruits from "./fruits.js"; 


Default Imports

When importing a default value, we need to assign a name to it. Since it is the default, it doesn’t matter what we name it.

import fruitBasketList from "./fruitBasket.js";

We can also import all of the exports, including named and default exports, at the same time. This will put all of them exports into an object, and the default export will be given the property name “default”.

import * as fruitBasket from "./fruitBasket.js"; 

Named Imports

We can import any named export by wrapping the exported name in curly brackets.

import { fruitBasket, Apple } from "./fruitBasket.js";

We can also rename the import as we import it using the as keyword.

import {fruitBasket as basketOfFruit, Apple} from './fruitBasket.js`

We can also mix named and default exports in the same import statement. The default export is listed first, followed by the named exports in curly brackets.

import fruitBasket, { Apple } from "./fruitBasket.js";

Finally, we can import a module without listing any of the exports we want to use in our file. This is called a ‘side-effect’ import, and will execute the code in the module without providing us any exported values.

import "./fruitBasket.js";

Dynamic Imports

Sometimes we don’t know the name of a file before we import it. Or we don’t need to import a file until we are partway through executing code. We can use a dynamic import to import modules anywhere in our code. It’s called “dynamic” because we could use any string value as the path to the import, not just a string literal.

Since ES Modules are asynchronous, the module won’t immediately be available. We have to wait for it to be loaded before we can do anything with it. Because of this, dynamic imports return a promise which resolves to our module.

If our module can’t be found, the dynamic import will throw an error.

async function createFruit(fruitName) { try { const FruitClass = await import(`./${fruitName}.js`); } catch { console.error("Error getting fruit class module:", fruitName); } return new FruitClass();