Default Export Namespace Export
In the ecosystem of ES Modules (ESM), the way you export members from a file defines how other developers—and your future self—will interact with your code. Choosing between default exports and namespace exports is not merely a matter of syntax; it is a strategic decision that influences API clarity, maintainability, and the efficiency of build tools.
At its core, this choice represents two different philosophies: one focuses on a "single primary entry point," while the other focuses on "logical aggregation."
A default export is used to designate the "main" functionality of a module. When you use export default, you are telling the consumer, "If you only want one thing from this file, this is it."
Syntax and Flexibility
A module can have at most one default export, though it can coexist with any number of named exports.
// math.js
export default function add(a, b) {
return a + b;
}
export const PI = 3.14159;
One of the most distinctive features of default exports is the freedom of naming during the import process. Because the export is the "default" value, the importer can assign it any name they choose:
import add from './math.js';
import sum from './math.js'; // Also valid
console.log(add(1, 2)); // 3
console.log(sum(3, 4)); // 7
While this flexibility is powerful, it carries a risk: it relies heavily on developer convention. Without strict team standards, the same module might be imported as add, sum, or calculate in different parts of a codebase, which can hinder searchability and consistency.
When to use Default Exports
Default exports are most effective when a module has a single, clear responsibility. Common use cases include:
- React Components: Where the component is the sole purpose of the file.
- Configuration Objects: A single object containing environment settings.
- Core Classes: A single class representing a specific entity.
Mastering Namespace Exports
Namespace exports (often implemented via the export * as ns syntax introduced in ES2020) are designed for aggregation. Instead of exposing individual pieces, you bundle a collection of related capabilities into a single, cohesive object.
Aggregating Capabilities
This pattern is particularly useful for utility libraries or complex modules where you want to provide a structured API.
// string-utils.js
export function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function trim(str) {
return str.trim();
}
export default function slugify(str) {
return str.toLowerCase().replace(/\s+/g, '-');
}
You can then aggregate these in an entry point (like an index.js file) to create a clean, organized API:
// utils/index.js
export * as stringUtils from './string-utils.js';
export * as numberUtils from './number-utils.js';
When consuming this aggregated module, the developer interacts with a structured object:
import { stringUtils, numberUtils } from './utils/index.js';
console.log(stringUtils.capitalize('hello')); // Hello
console.log(stringUtils.trim(' hi ')); // hi
// Note the handling of the default export:
console.log(stringUtils.default('Hello World')); // hello-world
The "Default" Nuance
It is critical to remember that when you use export * as ns, the source module's default export is not lost; it is simply tucked away under the .default property of the namespace object. This is a common point of confusion for developers transitioning from older module patterns.
Key Differences at a Glance
To help decide which pattern to apply, consider the following comparison:
| Feature | Default Export | Namespace Export |
|---|---|---|
| Quantity | Maximum one per module | Multiple namespace objects allowed |
| Import Naming | Arbitrary (chosen by importer) | Fixed (must match exported name) |
| Access Pattern | Direct variable usage | Property access (ns.member) |
| Default Handling | The primary value | Becomes the .default property |
| Static Analysis | Can be harder for some tools | Highly structured and predictable |
| Best Use Case | Single-purpose modules/components | Utility grouping and API organization |
A Critical Distinction: export * vs. export * as ns
It is easy to confuse these two, but they behave very differently:
export * from './mod.js': Re-exports all named exports from the source. It ignores the default export.export * as ns from './mod.js': Creates a namespace object containing everything, including the default export (asns.default).
Advanced Patterns and TypeScript
Mixed Exports
In sophisticated library design, you will often see both patterns used together. A module might export a primary class as a default, while also providing several helper constants as named exports, which are then aggregated via a namespace in the public API.
// index.js
export { default as MainService } from './service.js';
export * as Helpers from './helpers.js';
TypeScript Considerations
If you are working in a TypeScript environment, ensure your module setting in tsconfig.json is set to es2020 or higher to support the export * as ns syntax.
Furthermore, do not confuse the ESM Namespace Export with the TypeScript namespace keyword. The namespace keyword is a TypeScript-specific construct used to group code for type-checking and organization, which often compiles down to IIFEs (Immediately Invoked Function Expressions). For modern web development, you should prioritize the standard ES Module namespace export (export * as ns) to ensure compatibility with standard bundlers and browsers.
Professional Best Practices
To maintain a high-quality codebase, follow these architectural guidelines:
- Prioritize Named/Namespace Exports for Libraries: If you are building a utility library, avoid default exports. Named and namespace exports are much friendlier to tree-shaking (the process of removing unused code), as bundlers can more easily track exactly which functions are being used.
- Use Default Exports for "Singletons": If a file's only job is to export a React component or a specific configuration, a default export is semantically appropriate.
- Avoid Deep Nesting: While namespace exports are great for grouping, avoid creating overly deep hierarchies like
utils.string.format.trim. This creates friction for the developer and makes the code harder to read. Aim for a shallow, logical structure. - Enforce Naming Conventions: If your team uses default exports frequently, establish a convention for how they should be named during import to prevent the "naming chaos" mentioned earlier.
- Think in Terms of Intent: Before writing an
exportstatement, ask yourself: "Is this file a single tool, or is it a toolbox?" If it's a tool, usedefault. If it's a toolbox, use namespaces.