import / export
Introduction
import and export are the declarations that make ES Modules work. Exports decide what a module exposes; imports decide which bindings another module consumes.
The syntax looks simple, but interviews probe details: named versus default exports, aliases, namespace imports, re-exports, barrels, live read-only bindings, and why static imports cannot be placed inside if blocks.
Why This Matters
Clear module boundaries are a production engineering skill. Good import/export structure improves bundle size, testability, dependency direction, and code review readability. It also prevents common mistakes such as default/named import mismatches and circular barrel dependencies.
Theory
Named exports
Named exports expose one or more bindings by name. Importers must use the exported name or an alias: import { formatDate as format } from './date.js'. Named exports are best when a module has several public utilities.
Default exports
A module can have one default export. Importers choose any local name: import Button from './Button.js'. Defaults are common for primary components or classes, but named exports often refactor better because tooling can track names more explicitly.
Namespace imports
import * as date from './date.js' collects the module namespace into an object-like value. It is useful when you want to group related exports, but it can make tree-shaking less obvious in some toolchains if overused.
Re-exports and barrels
A barrel file re-exports from several modules, often through export { Button } from './Button.js'. Barrels improve public APIs but can create accidental cycles or pull in side effects if used carelessly.
Static import constraints
Static imports must be top-level. Use dynamic import() when you need conditional or lazy loading. Static imports are hoisted and linked before the module body evaluates.
Visual Diagrams
date.js exports: formatDate parseDate default locale consumer choices: named import -> one binding by exported name default import -> the module's primary value namespace import -> object-like namespace of exports re-export -> pass bindings through another module
Choosing the right import/export form makes module APIs easier to read and optimize.
Code Examples
Named, default, alias, and namespace imports
This read-only sample uses separate files. Static imports belong at the top level of an ES module.
Re-exports and barrel files
Barrels define a stable public API, but avoid hiding large side-effectful dependency graphs behind one file.
Dynamic import for conditional loading
Static imports are top-level. When code must load conditionally or lazily, use dynamic import in an async boundary.
Playground
Press Run to execute the code and see output here.
Output Prediction
Predict the output #1
1let internal = 1;2const namespace = {};3 4Object.defineProperty(namespace, 'value', {5 get: function () {6 return internal;7 }8});9 10const snapshot = namespace.value;11internal = 2;12 13console.log(snapshot);14console.log(namespace.value);Coding Exercises
Classify import requests
MediumImplement classifyImports(requests) where each request is an object with kind equal to 'named', 'default', or 'namespace'. Return a count object with keys named, default, and namespace.
Interview Questions
1What is the difference between named and default exports?
A module can have many named exports, imported by their exported names or aliases. A module can have only one default export, imported with any local name. Named exports usually improve refactoring and discoverability; default exports are convenient for a module's single primary value.
2Can you put a static import inside an `if` statement?
No. Static import declarations must be at the top level of an ES module so the module graph can be linked before evaluation. For conditional or lazy loading, use dynamic import().
Follow-ups
- What does a namespace import contain?
- When can barrel files hurt a codebase?
3Why might a named/default import mismatch fail?
Named imports must match named exports exactly unless aliased. Default imports read the module's default export. Importing { Button } from a module that only has export default Button asks for a named export that does not exist.
Quiz
1. How many default exports can one ES module have?
2. Which statement about static imports is true?
Summary
- Named exports expose bindings by name; default export exposes one primary value.
- Imports can alias named exports and can collect exports through namespace imports.
- Re-export barrels shape public APIs but can hide dependency and side-effect costs.
- Static imports must be top-level; dynamic `import()` handles conditional or lazy loading.
- Imported ESM bindings are live read-only views.
Cheat Sheet
Named: export const x = 1; import { x } from './m.js'.
Alias: import { x as value } from './m.js'.
Default: export default Thing; import Thing from './Thing.js'.
Namespace: import * as api from './api.js'.
Re-export: export { Button } from './Button.js'.
Dynamic: const mod = await import('./heavy.js').