JAVASCRIPT / MODULES AND DEVELOPER TOOLING
Importing modules and namespace objects
Import named, default, namespace, and side-effect-only bindings from other modules, and reason about the live, read-only namespace object they produce.
What you will learn
- Choose between named, default, namespace, and side-effect-only import forms
- Rename imports with `as` when two modules export the same name
- Read a namespace object as a live, read-only, null-prototype view of exports
- Explain why a misspelled import name fails before any of your code runs
Understanding Importing modules and namespace objects
An import declaration is a wiring instruction, not an assignment. `import { count } from './counter.mjs'` creates a local name `count` that points at the exporting module's own `count` binding, so when that module reassigns it your name sees the new value with no extra work. The wiring is one-way: the importer may read the binding but never write to it, which is exactly what keeps a module's exported state changeable only by code inside that module.
There are four forms and they combine freely. Braces pull in named exports by their exported names; a bare identifier before the braces is shorthand for the export named `default`; `* as ns` gathers every export into one namespace object where the default sits at `ns.default`; and a specifier with no bindings at all, like `import './setup.mjs'`, just evaluates the module for its side effects. The braces look like destructuring but are not: no computed keys, no default values, no nesting, because those names are matched against the target's export list rather than looked up on a runtime object.
Loading a module graph happens in phases. The engine parses each file, resolves every specifier and matches every imported name to an actual export (linking), and only then evaluates module bodies depth-first. That is why import declarations are hoisted and their position in the file is irrelevant, and why a misspelled import name is a SyntaxError before one line of your code runs. The object from `* as ns` is built during that same phase as an exotic object: null prototype, non-extensible, keys in sorted order, each property a live read-only view of one export.
// --- counter.mjs ---
export let count = 0;
export function increment() {
count += 1;
}
export default function label() {
return `count is ${count}`;
}
// --- main.mjs --- run with: node main.mjs
import label, { count, increment } from './counter.mjs';
import * as counter from './counter.mjs';
console.log(count, counter.count);
increment();
console.log(count, counter.count);
console.log(label());
console.log(Object.keys(counter));
console.log(counter[Symbol.toStringTag]);An import declaration wires read-only live bindings, resolved at link time, into your module's scope; it never copies values.
Worked examples
Renaming to survive a name collision
Two modules export the same name, and `as` decides what each one is called locally.
// --- mathA.mjs ---
export const version = 'A';
export function area(r) {
return 3 * r * r;
}
// --- mathB.mjs ---
export const version = 'B';
export function area(r) {
return Math.PI * r * r;
}
// --- main.mjs ---
import { version as versionA, area as roughArea } from './mathA.mjs';
import { version as versionB, area } from './mathB.mjs';
console.log(versionA, versionB);
console.log(roughArea(2));
console.log(area(2).toFixed(4));Example explained
Line 1`version as versionA` renames only at the import site; mathA.mjs still knows it as `version`.
Line 2Both modules can export `version` because uniqueness is required of the local names in main.mjs, not of export names across files.
Line 3`area` from mathB.mjs is imported unrenamed, so the plain name refers to the accurate implementation.
Line 4`roughArea(2)` returns 12 because mathA's version multiplies by 3 instead of Math.PI.
What the namespace object actually is
A namespace object is an exotic, sealed, prototype-less view rather than a plain object of copied values.
// --- config.mjs ---
export const mode = 'dev';
export const port = 8080;
// --- main.mjs ---
import * as config from './config.mjs';
console.log(config.mode, config.port);
console.log(config.missing);
console.log(Object.isExtensible(config), Object.getPrototypeOf(config));
try {
config.mode = 'prod';
} catch (err) {
console.log(err.name);
}Example explained
Line 1`config.missing` is a normal property miss and yields `undefined`, whereas `import { missing }` would have failed at link time.
Line 2`Object.isExtensible` is false, so no property can be added to the namespace object.
Line 3The prototype is `null`, which is why inherited helpers such as `hasOwnProperty` are absent.
Line 4Assignment throws because writes to a namespace property always fail and module code is strict.
Imports are hoisted, dependencies evaluate first
An import placed after other statements still runs before them, because dependency evaluation precedes the module body.
// --- dep.mjs ---
console.log('dep evaluated');
export const value = 42;
// --- main.mjs ---
console.log('main body starts');
import { value } from './dep.mjs';
console.log('value =', value);Example explained
Line 1'dep evaluated' prints first: the graph is linked, then dep.mjs is evaluated, and only then does main.mjs's body start.
Line 2The import declaration is hoisted, so writing it below the first `console.log` changes nothing about when it takes effect.
Line 3By the time `value` is read, dep.mjs has finished, so the `const` binding has left its temporal dead zone.
Line 4Style tools flag imports placed mid-file; this ordering is here only to make the hoisting visible.
Important notes
The specifier must be a literal string; `import x from './' + name` is a SyntaxError, because specifiers are resolved before any code executes.
A namespace object has a null prototype, so `ns.hasOwnProperty('x')` throws; use `Object.hasOwn(ns, 'x')` or `'x' in ns`.
Common mistakes
Importing a default export by the exporting function's name, as in `import { config } from './config.mjs'` for a file that uses `export default`: linking fails with "does not provide an export named 'config'". Write `import config from './config.mjs'` or `import { default as config }`.
Trying to update shared state by writing to an import, either `count = 5` or `ns.count = 5`: both throw a TypeError because imported bindings and namespace properties are read-only. Export a mutator function from the owning module and call that instead.
Dropping the file extension in a relative specifier (`'./counter'`): Node's ES module resolver does no extension guessing, so it fails with ERR_MODULE_NOT_FOUND even though the file exists.
Try it yourself
Change, predict, then run
In a sandbox that supports multiple files, write `metric.mjs` and `imperial.mjs` that each export a `format` function plus a default value, then in `main.mjs` import one as `{ format as formatMetric }` and the other as `* as imperial`. Log `Object.keys(imperial)` and `imperial.default`, and check where `default` lands in the key order.
Open the JavaScript workspaceCheck your understanding
A module begins with `console.log('start')` and, on the next line, imports `{ helpr }` from a file whose only export is `helper`. Nothing is printed and the program fails immediately. What explains the missing output?
- console.log output is discarded when the module later throws
- The import declaration is hoisted above the log, so it executes first and throws
- Import names are matched against the target's exports while the graph is linked, before any module body is evaluated
- Import names are resolved lazily, so the error surfaces the first time `helpr` is used
Show answer
Linking resolves every specifier and checks every imported name across the whole graph before evaluation begins, so the failure happens before `start` could ever print. Hoisting is a real effect but does not explain this case: the error appears even if the import sits on the last line, because the import never needed to "execute" at its position. And static imports are not lazy, which is why the typo cannot survive until first use.