Grouped Export Namespaces: `export { foo, bar } as qux`

Goal:
Allow grouping multiple exports under a common namespace at export time, without needing wrapper objects or intermediate files.

Idea 1: Object-like export grouping

const foo = () => {}
const bar = 33
export { foo, bar } as 'qux'

Similar to :

  • export const qux = { foo, bar }

  • import * as qux from 'qux'

Considerations:

  • Such a proposal would elevate as into a clear boundary marker between local clarity and global coherence, letting authors keep intuitive local names while exporting under meaningful, conflict-free namespaces.

  • Using import * as qux from 'qux' is a consumer’s choice whereas export { foo, bar } as qux (if it existed) would be a maintainer’s decision, a deliberate part of the module’s public API design, shaping how the module is meant to be consumed.

  • Grouping export statements at the end of a file improves readability by centralizing the module’s public API, so you don’t have to scroll and hunt for scattered export keywords.


Primarily designed for libraries, a real-world use case could be:

// bezier.js
function quadratic() { ... }
function cubic() { ... }
function quartic() { ... }

export { quadratic, cubic, quartic } as 'bezier'

Other syntax options:

Idea 2: Inline Namespace Export

const foo = () => {}
const bar = 42

export namespace qux {
  export { foo, bar }
}

Idea 3: Object-like export grouping

export group qux {
  export const foo = ...
  export function bar() { ... }
}

For all these, the consuming module could write:

import { qux } from 'mymodule'

qux.foo()

Final thought:

My personal inclination leans toward idea 1; I do still wonder about readability in nested as cases like:

export {
  foo,
  bar as baz,
} as 'qux'

—but honestly, even that feels readable to me.

1 Like

How exactly is your proposed feature different from that? What does it do if not create a wrapper object? Or if it does, why would that new syntax?

You’re right that export { foo, bar } as qux isn’t much more than syntactic sugar over export const qux = { foo, bar }, but it introduces a few important distinctions:

Identity preservation: In export const qux = { foo, bar }, IDEs and tooling often lose track of the original bindings — whereas with export { foo, bar } as qux, the connection to the original foo and bar is preserved, making navigation, documentation, and type inference more precise.

Refactor-friendliness: Renaming foo only needs to happen once at its declaration, without worrying about object property keys staying in sync — much easier and safer in large codebases.

Semantic clarity & consistency: It mirrors the behavior of import * as qux, offering the same read-only, frozen, live-binding namespace object — but from the export side, giving the maintainer full control over how the module is meant to be consumed.

• And one more advantage: for larger libraries , this pattern allows maintainers to assemble a coherent namespace from multiple source files :

import { foo } from './foo.js'
const bar = 42

export { foo, bar } as qux

This way, you can build the qux namespace across several internal modules, while still offering consumers a single, clean entry point. Crucially, code navigation remains intact — tools can still trace qux.foo back to ./foo.js, preserving a clear map of where things come from, even when the public API is centralized.

Binding semantics are governed by the spec - nothing loses track of them.

Typically tests and/or types are how you avoid refactoring hazards.

import * as is only for laziness or metaprogramming, and i’m not sure why it’d be a benefit to encourage either.

That's seems a bit strong. Sometimes it's helpful for the reader to preserve the namespace of where an import comes from to provide extra context at the call site.

For example a project might be using two different versions of something, maybe it's migrating from one to the other. I do not think it's lazy to name an import namespace in all cases.

2 Likes

I’d gently push back on the idea that import * as is just about laziness or metaprogramming — in many cases, it’s a deliberate design choice to expose a cohesive namespace while preserving modular structure and tree-shaking friendliness.

But thinking of it, i guess there is one more argument to expose here: Tree Shaking

For example, imagine a module that exports various Bézier curve utilities:

export { quadratic, cubic, quartic, quintic } as bezier

This allows consumers to write bezier.cubic(...), which is semantically meaningful, preserves the original identity of cubic, and enables tree-shaking: if only bezier.cubic is used, a bundler can omit the rest (quadratic, quartic, etc.).

So it’s not just syntactic sugar — it encourages clean, modular usage without forcing consumers to flatten or manually destructure everything.

That said, this is clearly an opinionated stance, and I’m totally fine if it’s something that only fits my workflow or design philosophy — not trying to universalize it, just hoping to explore what’s possible.

1 Like

If I'm understanding what you're asking for correctly, you can accomplish this already by splitting out the values you want to export (and only those values) into their own file, and then doing export * as 'qux' from 'other-file.js', yes?

(As of https://tc39/ecma262#1005.)

If that gives you exactly what you're asking for already, but with the caveat of requiring a separate file, then I think it's not unreasonable to have syntax sugar for this.

1 Like

That said, we might someday get module declarations (I know it looks like there's been no movement on this proposal, but there has been movement on proposals which need to get in first [1, 2]).

At that point you could write

module bezier {
  export function quadratic() {}
  export function cubic() {}
}

export * as 'bezier' from bezier;

with no new syntax needed. So, assuming that would accomplish what you want, maybe best not to invent any new syntax for now.

3 Likes

Exactly! It’s technically achievable today using an intermediate file, but that’s not always convenient.

I think there’s a broader question behind this: should the JS community support some kind of namespace export that goes beyond plain objects — something more like what import * as xxx gives us — but from the export side? Something that may support tree-shaking in the future.

Take three.js, for example — many projects only use a small portion of it, maybe 20%. But because flat imports are often the only ergonomic option, you end up with something like:

import {
  BufferGeometry,
  Color,
  ColorRepresentation,
  DepthTexture,
  DoubleSide,
  Group,
  Material,
  Mesh,
  MeshBasicMaterial,
  OrthographicCamera,
  PerspectiveCamera,
  PlaneGeometry,
  Scene,
  ShaderMaterial,
  TorusKnotGeometry,
  UnsignedShortType,
  WebGLRenderer,
  WebGLRenderTarget
} from 'three'

Allowing to use a namespace while keeping the advantage of tree shaking, is such a topic already discussed?

export { banana, gorilla } as jungle

In other words, the idea is that JavaScript might one day let you have the banana — from the jungle — without dragging in the gorilla and the entire jungle .

So consumers can still write jungle.banana, but only include what they actually use.

@jniac
Do you mean you want to group variables and make them public?

// module.js
let foo = 0;
let bar = 1;
export function set({foo: foo_,bar: bar_} ={}) {
  if ((foo_ ?? undefined) !== undefined) {
    foo = foo_;
  }
  if ((bar_ ?? undefined) !== undefined) {
    bar = bar_;
  }
}
export { foo, bar } as 'model';
// client.js
import { model, set } from "./module.js";
console.log(model); // => {foo: 0, bar: 1};
set({foo:100, bar: 200});
console.log(model); // => {foo: 100, bar: 200};

This is essentially the same as:

// module.js
export let model= {foo: 0, bar: 1};
export function set({foo: foo_,bar: bar_} ={}) {
  if ((foo_ ?? undefined) !== undefined) {
    model.foo = foo_;
  }
  if ((bar_ ?? undefined) !== undefined) {
    model.bar = bar_;
  }
}

The public variable model is not prohibited from being changed, so maybe that's not the case...

I'm a fan of the idea. I was already wondering myself if there is a way to overcome all the issues that you mentioned.

  • single export namespace ideally at end of file
  • tree shaking possibilities
  • improved IDE navigation

I was mainly using/teaching/suggesting to use the

export const myNamespace = { foo, bar };

even though I knew all the drawbacks, because it still improved readability and maintainability of the whole code base.

Maybe a little off-topic but there is one other pattern, we're using for organizing unit tests, which is:

// only exported for testing
export const _myNamespace = { one, two };

Namely, exports that should be only available for testing. I'd fear with your suggestion, I'd still need to do that, in order to mock these kind of functions when importing the module non-lazy in my tests, right?


Nevertheless, I like the idea.

The best way to avoid treeshaking is not importing more than you need in the first place. There's tons of articles about why barrel files are bad, and deep imports are The Way.

I'd suggest reading those.

Hey @ljharb,

Sorry, but this is not helpful. I'm aware of these. I mentioned it AND the reasons why we still ignore these issues. Especially, when not working on packages but rather end product development.

1 Like

Yes, and even in end product development, you can drastically reduce memory footprint and/or bundle size by using deep imports instead of barrel files.

Sure, but we don't use any barrel imports. Working on a code base with hundreds of engineers and more than 70 micro frontends, code navigation, readability and maintainability are for us currently more important than the other benefits.

I see the need of using the less readable deep imports to get these improvements rather as a limitation of the language.

That's why I think, the suggestion makes sense as it potentially could improve the language.

But I don't know enough about necessary compiler or bundler modifications to actually make that happen.

I get the concern about barrel files, but I think that’s a slightly different issue. What I’m proposing isn’t about masking module structure — it’s about creating grouped exports that are still statically analyzable.

If identity is preserved (i.e. the binding still points directly to the original function or variable), then tree-shaking shouldn’t be a blocker — it’s already working today in scenarios like:

export { banana, gorilla }

Bundlers like Rollup and ESBuild are already capable of removing unused named exports — so if a grouped export like:

export { banana, gorilla } as jungle

behaves the same way under the hood (i.e. exposing a namespace object with live bindings), then tree-shaking logic can remain almost identical — just one level deeper in analysis.

In fact, the long-term goal should be: enable clean, namespaced APIs without giving up on tree-shaking. That’s where we can offer better DX and better performance.

Encouraging deep imports is fine as a short-term optimization, but it’s not a great developer experience — especially in large libraries (e.g. three.js) where each symbol has be imported separately, ending in large imports.

1 Like

Another small but real advantage of grouped exports is that they make the coding experience smoother in some editors.

Right now, if you comment out a single usage of a deeply imported symbol, many IDEs (like VSCode) will automatically remove the import if it’s the only reference. Then, when you uncomment the line to fix the issue, the symbol is gone and you have to re-import it manually.

With a grouped namespace import (e.g. bezier.cubic), the IDE won’t touch the parent import — so you can freely comment/uncomment lines during debugging or iteration without losing your context. It’s a subtle thing, but it adds friction when you’re iterating fast.

Grouped exports would help preserve that continuity.

1 Like