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 (as ns.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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Think in Terms of Intent: Before writing an export statement, ask yourself: "Is this file a single tool, or is it a toolbox?" If it's a tool, use default. If it's a toolbox, use namespaces.