skip to content

In an ES module, what does `export * from './utils.js'` actually re-export, and what does it leave out?

level: middleimportance: should knowfreq 50%

answer

  1. star forwards names, not everything
  2. one export is deliberately left behind
  3. nothing lands in the local scope
  4. two stars, same name, ambiguous
  5. export * as ns is a different statement

basics

~10 s

It forwards every named export of ./utils.js under the same names, and deliberately excludes that module's default export. It also creates no local bindings, so the re-exporting file cannot use those names itself.

solid answer

~50 s

`export * from './utils.js'` adds every **named** export of the target to the current module's export list, under the same names. The default export is explicitly not included — if you want it forwarded you must say so with `export { default } from './utils.js'` or `export { default as Utils } from './utils.js'`. The star also creates nothing in the local scope: the re-exporting file cannot reference those names in its own code. Two important collision rules: an explicit export declared locally takes precedence over a star-provided name of the same spelling, and if two `export *` declarations supply the same name from different bindings, that name becomes ambiguous — it is dropped from the star set, and any module that tries to import it fails at link time. `export * as utils from './utils.js'` is a different statement: it exports one name whose value is the target's namespace object, and that object does include `default`.

code

javascript · 11 lines
javascript
// button.js
export const SIZES = ['sm', 'lg'];
export default class Button {}

// index.js  (barrel)
export * from './button.js';                      // forwards SIZES only
export { default as Button } from './button.js';  // default must be explicit

// consumer.js
import { SIZES, Button } from './index.js';
console.log(SIZES, typeof Button); // ['sm','lg'] 'function'

go deeper

for a junior

Know that this line forwards another module's named exports so consumers can import them from one place, and that the default is not carried along automatically.

for a middle

Explain the four mechanics: same names forwarded, default excluded, no local binding created, and duplicate names across two stars becoming ambiguous rather than silently resolved.

for a senior

Weigh star re-exports against explicit lists for a package's public surface — implicit API growth, where collision errors surface, and the fact that starring a folder still evaluates every file in it.

for a principal

Own the barrel policy for a shared package: what is exported at all, how the surface is kept intentional as it grows, and how a rename inside the package is prevented from becoming an unannounced break for consumers.

## What the star form is for `export * from './utils.js'` is a re-export declaration. It says: everything this other module exposes by name, my module now exposes too, under the same names. It exists mainly for barrel files — an `index.js` that gathers a folder's public surface into one specifier so consumers write `import { parse, format } from './text/index.js'` instead of reaching into individual files. ```js // text/index.js export * from './parse.js'; export * from './format.js'; ``` ## The default export is not included This is the fact interviewers are usually fishing for. The star forwards named exports only; the target's `default` is skipped. It has to be: if it were included, two starred modules that both have a default would collide on the name `default` in every barrel, and the barrel's own default would be impossible to control. Forwarding a default is therefore an explicit act: ```js export { default } from './button.js'; // this module's default is button's export { default as Button } from './button.js'; // exposed as a named export instead ``` ## No local bindings are created A re-export is not an import. `export * from './parse.js'` does not put `parse` into the barrel's scope, so code inside the barrel cannot call it — referencing it throws a ReferenceError. If the file needs to *use* a value as well as forward it, it must import it separately. ## Collisions and ambiguity Two rules govern name clashes, and both are worth being able to state. First, a local or explicit export always wins over a star. Given: ```js export * from './a.js'; // a.js exports parse export const parse = myOwnParse; ``` consumers get the locally declared `parse`; the starred one is shadowed, not an error. Second, if two star declarations supply the same export name from **different** bindings, the name is ambiguous. It is excluded from the star set, and it is not merely undefined: a module that explicitly imports that name from the barrel fails during linking with a SyntaxError, before anything runs. Importantly, if both stars ultimately resolve to the *same* binding — for example both modules re-export it from a common source — there is no ambiguity at all. The practical consequence is that a barrel built from many `export *` lines is quiet about collisions until someone imports the colliding name, at which point the error points at the consumer rather than at the barrel that caused it. Explicit `export { … } from` lists cost a few more keystrokes and make the surface reviewable. ## The namespace re-export is a different statement ```js export * as utils from './utils.js'; ``` This was added in ES2020, and it does something else entirely: it exports **one** name, `utils`, whose value is the target module's namespace object. Consumers write `import { utils } from './index.js'` and reach members as `utils.parse`. Because a namespace object exposes every export as a property, `utils.default` *is* available here — the exclusion described above applies to the bare star form, not to a namespace object. ## Ordering and evaluation A re-export is not conditional and cannot be nested inside a block or a function; like every import and export declaration it must sit at the module's top level. And starring a module still causes that module to be evaluated when the graph runs, so a barrel that stars a dozen files pulls all of them in, even if the consumer only wanted one name from one of them. That is a cost to weigh against the ergonomics, especially for a large public barrel. ## What to say in an interview A complete answer names four things: all named exports are forwarded under the same names; the default is excluded and must be forwarded explicitly; no local binding is created; and duplicate names across two stars become ambiguous rather than last-one-wins. Adding that `export * as ns from` is a distinct form that exports a single namespace-object name shows you know the ES2020 addition rather than confusing it with the bare star.

  • Two `export *` lines in a barrel both provide a name called `format`. What happens?
    If they resolve to different bindings the name is ambiguous: it is excluded from the barrel's star exports, and any module that explicitly imports `format` from the barrel fails at link time with a SyntaxError. If both stars trace back to the same original binding there is no conflict. Declaring `export { format } from './one.js'` explicitly in the barrel resolves it, since explicit exports take precedence.
  • Why would a team prefer explicit `export { a, b } from './x.js'` lists over `export * from './x.js'` in a barrel?
    Because the star makes the public surface implicit: adding an export in a leaf file silently widens the package's API, and name collisions only surface when a consumer trips over them. An explicit list is reviewable in a diff, keeps the surface intentional, and puts any conflict error in the barrel where it was caused.

saying these in an interview costs you the question

  • Says export * also forwards the default export
  • Thinks a duplicate name is resolved last-one-wins
  • Assumes the re-exporting file can call the starred names
  • Confuses export * as ns with the bare export * form
  • Believes a barrel avoids evaluating the starred modules

context