Export and Import

The export and import keywords come in more than one shape. The previous article showed the plain case: mark a function with export, pull it into another file with import. Here you’ll meet the full set of variations, and learn when each one earns its place.

Think of it as a small vocabulary. Once you can read every form at a glance, module files stop looking like boilerplate and start reading like a table of contents.

export — the source file
before a declaration
as a standalone list
renamed with as
one default per file
re-exported from elsewhere
import — the consuming file
named, in { }
the default, no braces
everything as * as obj
renamed with as
for side effects only
The two sides of a module boundary and the forms each side can take.

Export before a declaration

You can turn any declaration into an export by writing export in front of it. It works for variables, functions, and classes alike.

Every one of these is valid:

// export an array
export let planets = ['Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter', 'Saturn'];

// export a constant
export const MAX_RETRIES = 3;

// export a class
export class Member {
  constructor(name) {
    this.name = name;
  }
}

The declaration still does its normal job. export let planets creates planets in the module’s own scope exactly as let planets would; the keyword just also publishes it under the name planets for other files to import. Nothing about the value or the local binding changes.

Export separately from a declaration

export doesn’t have to sit in front of the thing it exports. You can declare everything first, then publish a list of names at the end:

// 📁 alerts.js
function chime(user) {
  alert(`Ping for ${user}!`);
}

function buzz(user) {
  alert(`Buzz for ${user}!`);
}

export {chime, buzz}; // a list of exported names

This is handy when you want a single spot that spells out the module’s public surface. Anyone opening alerts.js can jump to that one line and see exactly what leaves the file, without scanning every function for an export prefix. Putting export above the functions would work too — this is a matter of taste, not correctness.

📁 alerts.js
function chime(u) { … }
function buzz(u) { … }
export { chime, buzz }
chime, buzz
───→
public names
Two functions defined privately; one export line decides what the outside world sees.

Import a list of names

The everyday form lists what you want inside curly braces:

// 📁 main.js
import {chime, buzz} from './alerts.js';

chime('Maya'); // Ping for Maya!
buzz('Maya'); // Buzz for Maya!

Each name in the braces has to match a name the module exported. The braces here are import syntax, not an object literal — you’re selecting entries from the module, not destructuring a value.

Import everything as an object

When a module exports a lot, you can grab all of it under one namespace with import * as <obj>:

// 📁 main.js
import * as alerts from './alerts.js';

alerts.chime('Maya');
alerts.buzz('Maya');

Now every export hangs off alerts as a property. It looks tempting — short to type, and you never have to update the import when you start using one more function. So why bother listing names by hand?

Two reasons hold up in practice:

  1. Shorter call sites. A named import gives you chime() instead of alerts.chime(). Over a file, that noise adds up.
  2. A readable dependency list. An explicit import {a, b, c} documents precisely what this file uses. That makes the code easier to follow, and far easier to refactor — when you delete a function, its unused import stands out.

Import “as”

Add as to bind an import to a different local name. Useful when a name is long, or when it would clash with something you already have:

// 📁 main.js
import {chime as ping, buzz as beep} from './alerts.js';

ping('Maya'); // Ping for Maya!
beep('Maya'); // Buzz for Maya!

The exported names are still chime and buzz — the module doesn’t know or care what you renamed them to. ping and beep exist only inside main.js.

Export “as”

The same renaming trick works on the export side. Here the module publishes its functions under outward-facing names:

// 📁 alerts.js
// ...
export {chime as ping, buzz as beep};

Now ping and beep are the official names other files import by:

// 📁 main.js
import * as alerts from './alerts.js';

alerts.ping('Maya'); // Ping for Maya!
alerts.beep('Maya'); // Buzz for Maya!
local in alerts.js
chime
export as ping
───→
public name
ping
import as …
───→
local in main.js
alerts.ping
as renames at the boundary — the local name inside the file is independent of the public name.

Export default

Modules tend to fall into two camps.

  1. A library module: a bundle of related functions, like alerts.js above.
  2. A single-entity module: one file whose whole purpose is one thing, say a member.js that exports just class Member.

The second style is the one most projects reach for — one “thing” per file. It means more files, but with clear names and a sensible folder layout that’s a help, not a burden. Finding code gets easier when the filename tells you what’s inside.

For that one-thing-per-file style, modules offer export default. Put it in front of the entity:

// 📁 member.js
export default class Member { // just add "default"
  constructor(name) {
    this.name = name;
  }
}

A file may have at most one default export.

Import it with no curly braces:

// 📁 main.js
import Member from './member.js'; // not {Member}, just Member

new Member('Maya');

That braceless form reads cleanly, and it’s the source of a classic beginner slip: forgetting the braces on a named import (or adding them to a default one). Keep the split straight — named exports need braces, the default export doesn’t.

Named export Default export
export class Member {...} export default class Member {...}
import {Member} from ... import Member from ...

A module can hold both a default and named exports, but mixing them is uncommon. Most files pick one style: all named, or a single default.

named
export { Member }
↓ must match
import { Member } from …
default (one per file)
export default class Member
↓ any local name
import Anything from …
A named export is matched by name; a default export is matched by position — you name it at import time.

Because there’s only ever one default per file, the exported value is allowed to be nameless. All of these are fine:

export default class { // no class name
  constructor() { ... }
}
export default function(user) { // no function name
  alert(`Ping for ${user}!`);
}
// export a single value, without making a variable
export default ['red', 'green', 'blue', 'amber'];

Leaving off the name causes no ambiguity: the importing file uses import without braces, and there’s exactly one default thing to bind.

A non-default export enjoys no such freedom. It has to be named, so this errors:

export class { // Error! (non-default export needs a name)
  constructor() {}
}

The “default” name

There’s a hidden identifier at work. Behind the scenes, the default export is registered under the name default, and a few situations let you refer to it that way.

You can export a function as the default separately from its definition:

function chime(user) {
  alert(`Ping for ${user}!`);
}

// same as if we added "export default" before the function
export {chime as default};

Or take the rarer case of a module that has one main default export plus a couple of named ones:

// 📁 member.js
export default class Member {
  constructor(name) {
    this.name = name;
  }
}

export function greet(member) {
  alert(`Hi, ${member}!`);
}

To pull in the default alongside a named export, address the default by its default name and rename it in the same breath:

// 📁 main.js
import {default as Member, greet} from './member.js';

new Member('Maya');

And if you import everything with *, the default lands on the default property of the namespace object:

// 📁 main.js
import * as member from './member.js';

let Member = member.default; // the default export
new Member('Maya');
📁 member.js exports
default → class Member
greet → function
───→
import * as member
member.default
member.greet
The default export lives under the internal name 'default' — visible as the .default property of a namespace import.

A word against default exports

Named exports pin down the name. To import one, you have to spell it exactly as exported:

import {Member} from './member.js';
// import {MyMember} won't work, the name must be {Member}

The default export flips that. You choose the local name, and any name is accepted:

import Member from './member.js'; // works
import MyMember from './member.js'; // works too
// could be import Anything... and it'll still work

That flexibility has a downside: two teammates can import the same thing under two different names, and the codebase drifts. Search for Member and you miss the file that called it MyMember.

The usual fix is a convention: name the import after the file, so the name is predictable even though the language doesn’t enforce it.

import Member from './member.js';
import LoginForm from './loginForm.js';
import func from '/path/to/func.js';
// ...

Some teams find that too flimsy and swear off default exports entirely. Even a single-export module gets a name, no default. It keeps imports consistent, and it makes re-exporting (next up) a little smoother.

Re-export

export ... from ... imports something and re-exports it in one move, optionally under a new name:

export {chime} from './alerts.js'; // re-export chime

export {default as Member} from './member.js'; // re-export default

Where does this pay off? Picture a package: a folder holding many modules. Some of them are the real product you want people to use; many are internal helpers that shouldn’t leak out. (Registries like NPM let you publish such packages, though you don’t have to publish to organize code this way.)

Say the layout looks like this:

auth/
    index.js
    member.js
    helpers.js
    tests/
        login.js
    providers/
        github.js
        facebook.js
        ...

You want a single front door. Someone using the package should import from one main file, auth/index.js, and never reach inside:

import {login, logout} from 'auth/index.js'

auth/index.js is that entry point. It gathers up everything the package offers and nothing it doesn’t. Outsiders get a stable surface; the folder’s internal shape stays private and free to change.

Since the real implementations are spread across files, index.js can import each one and re-export it:

// 📁 auth/index.js

// import login/logout and immediately export them
import {login, logout} from './helpers.js';
export {login, logout};

// import default as Member and export it
import Member from './member.js';
export {Member};
// ...

export ... from ... collapses each of those import-then-export pairs into a single line:

// 📁 auth/index.js
// re-export login/logout
export {login, logout} from './helpers.js';

// re-export the default export as Member
export {default as Member} from './member.js';
// ...
helpers.js login, logout
member.js default
───→
📁 auth/index.js
export { login, logout }
export { default as Member }
───→
outside code
import from index.js
index.js re-exports from internal modules, giving the package one public entry point.

One thing to watch: export ... from does not create a local binding. The re-exported names pass straight through the file — inside auth/index.js you can’t actually call login or logout as if you’d imported them. If you need to use them there too, do a separate import.

Re-exporting the default export

Default exports need extra care when you re-export them. Take a member.js with a default class:

// 📁 member.js
export default class Member {
  // ...
}

Two traps come up:

  1. export Member from './member.js' is a syntax error — that form doesn’t exist. To forward the default, you must write export {default as Member} from './member.js', as shown above.

  2. export * from './member.js' re-exports only the named exports and silently skips the default. If a file has both and you want everything forwarded, you need two statements:

    export * from './member.js'; // re-export named exports
    export {default} from './member.js'; // re-export the default export

These rough edges around forwarding a default are another reason some developers stick to named exports.

Summary

Every export form covered across this and the previous article. Read each one and check that you can say what it does:

  • Before a class/function/variable declaration:
    • export [default] class/function/variable ...
  • Standalone export:
    • export {x [as y], ...}
  • Re-export:
    • export {x [as y], ...} from "module"
    • export * from "module" (does not re-export the default)
    • export {default [as y]} from "module" (re-exports the default)

And the import forms:

  • Named exports:
    • import {x [as y], ...} from "module"
  • The default export:
    • import x from "module"
    • import {default as x} from "module"
  • Everything:
    • import * as obj from "module"
  • For side effects only — run the module’s code, bind nothing:
    • import "module"
In the sourceIn the consumer
export { x }import { x }
export default ximport Anything
export { x as y }import { y }
many exportsimport * as obj
A quick map from export form to the matching import form.

import and export can sit at the top or the bottom of a file — position doesn’t affect behavior. Module imports are resolved before the code runs, so even this works:

chime();

// ...

import {chime} from './alerts.js'; // import at the end of the file

By convention imports go at the top, purely because it’s easier to see a file’s dependencies at a glance.

One hard rule: import/export can’t live inside a block {...}.

A conditional import fails:

if (something) {
  import {chime} from "./alerts.js"; // Error: import must be at top level
}

The reason is that these statements are static. The engine reads them while parsing, before any if runs, so they can’t depend on runtime conditions. But what if you genuinely need to load a module only when it’s needed, or decide at runtime which one to load? That’s what dynamic imports are for, and they’re next.