18.6 Namespace Imports: import * as ns
Alright, let’s talk about grabbing the whole candy bowl instead of just one piece. The import * as ns syntax is your “I’ll take it all, thanks” move. It creates a single namespace object that contains all the exports from a module. It feels powerful, like you’re getting a great deal, but I need to be your brilliant friend here and tell you to use this power sparingly. It’s the culinary truffle of imports—potent, but a little goes a long way and it can ruin the dish if you’re not careful.
Here’s the basic, no-surprises usage:
// math-utilities.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const PI = 3.14159;
// main.js
import * as MathUtils from './math-utilities.js';
console.log(MathUtils.add(5, 3)); // 8
console.log(MathUtils.PI); // 3.14159
The MathUtils object now has properties add, subtract, and PI. Simple, right? But this is where the designers left us some… interesting choices to unpack.
It’s a Live Binding, Not a Snapshot
This is the most important thing to understand, and it trips up everyone coming from CommonJS. The namespace object you get is a live binding. This isn’t a frozen copy of the exports at the moment of import; it’s a dynamic proxy. If the exported value changes (which, admittedly, is rare and often a code smell), your namespace object reflects that change.
// counter.js
export let count = 0;
export const increment = () => count++;
// main.js
import * as Counter from './counter.js';
console.log(Counter.count); // 0
Counter.increment();
console.log(Counter.count); // 1 <-- It changed!
Now, contrast this with the CommonJS require() behavior, which gives you a copied object. This is a fundamental difference between the two systems and a huge reason why ES modules are more powerful for creating reactive, interconnected code.
The Default Export Gets Weird
Here’s the first truly questionable choice. What happens if your module has a default export?
The spec designers decided it should be placed on the namespace object under the key default. It’s a bit clunky.
// message-service.js
const defaultMessage = "Hello World";
export default defaultMessage;
export const specialMessage = "I'm special";
// main.js
import * as MessageService from './message-service.js';
console.log(MessageService.default); // "Hello World" <-- See? Weird.
console.log(MessageService.specialMessage); // "I'm special"
It’s honest, I’ll give them that. It’s not hiding the default export in some magical way. But it’s not elegant. You almost always want to import the default export directly (import defaultMessage from...) for clarity. Using it through a namespace object is like using a fork to eat soup—it works, but everyone will judge you for it.
When Should You Actually Use This?
Resist the urge to use import * everywhere because it feels convenient. It’s a code smell if you use it for everything. Here are the legitimate use cases:
Polyfills or Libraries that Patch the Global Environment: Some old-school libraries need to be imported for their side effects (e.g., attaching something to
windoworglobalThis). Usingimport *can make it explicit that you’re not using the exports directly but are invoking the module for its side effect.import * as SomeLegacyPolyfill from 'some-legacy-polyfill'; // This polyfill now adds SomeLegacyPolyfill to windowRe-exporting Everything: The most defensible use case. When you’re creating a barrel file (an
index.jsthat aggregates exports from many files),export * from...has limitations. It doesn’t let you control the namespace. Combiningimport *and a named export is perfect.// awesome-components/index.js import * as Button from './button.js'; import * as Card from './card.js'; export { Button, Card }; // Now you can import { Button } from 'awesome-components'Working with Dynamic Module Paths: When you use
import()for dynamic imports, it always returns a promise that resolves to the namespace object. So you’re forced to deal with it.const modulePath = `./widgets/${widgetName}.js`; const widgetModule = await import(modulePath); const MyWidget = widgetModule.default;
The main pitfall? Tree-shaking annihilation. If you use import * as Lib from 'big-lib' and then only use one function, most bundlers will be unable to shake out the other 99% of the library you didn’t use. They see you referencing the Lib object and have to assume you might use anything on it. Always prefer named imports (import { specificThing }) for optimal bundle size. Your users’ browsers will thank you.