ESM exports: named vs. default?
To name, or not to name?
Should you use named or default exports in JavaScript?
There’s no shortage of strongly worded articles on this topic.
The majority judge default export as “terrible.” Others maintain default should win (e.g. AirBnb style guide.)
They often blame entirely temporary things: IDE auto-import bugs, a particular bundler’s tree-shaking abilities, or the mere possibility of typos when naming an import.
Have we missed the point of exporting in the first place?
Code is Communication. ✨
We are sending a signal to
importers how to use a thing.
So, what are we saying?
Broadly speaking, there are 2 ways to export things in modern JavaScript:
- An
export defaultboldly declares “This is THE SINGLE MOST IMPORTANT thing.” Also, “any named exports only play a supporting role.” - A
named exportsays it’s “definitely A THING!” Also raises some questions, “got any other buddies there?“ Follow up, “Are they invited or required?”
Of course you can combine both, or use different approaches for different parts of your codebase. See more examples at the end of the article.
Weak Args, Man
Let’s address some of the common “temporary issues” folks run into.
- Arg #1: Named exports ensure name consistency. source
- No, they don’t. You’re maybe looking for a lint rule?
- (I hate to break it to you, but wait until you learn what variables can do!)
// You can alias using both!import { Knife as Handle } from "./knife.js"; // 🔪import { default as Handle } from "./knife.js"; // 🔪import Handle from "./knife.js"; // 🔪-
Arg #2: Use
import * as soManyKnives from './kinves.js'to combine named exports. (Not linked, author retracted.)- Neat feature. Not the point.
- Now tell me, how do I hold your contraption again? No author intent.
-
Arg #3: Named exports have better IDE import or renaming support. source
-
Incorrect (any more). Configure/update your tools.
-
Support has existed for 3+ years in VS Code, IntelliJ, etc.
-
Still, there are some “best practices” to use with
default exportsto get the best IDE & refactor experience. -
✅
export default function UserService() {}- always prefer named functions. -
❌
export default function() { }- anonymous functions are not implicitly tied to their filename. If you don’t name the thing, it’s hard to ask the computer to change it. -
Note: For historical reasons you cannot combine
export defaultwith aconstexpression.export default const Knife = () => {...blade, ...handle}// ^ ❌ Not Supported ❌ ^// Cannot export default const ....// ==========================// However, once declared you can export a const var as the default.const Knife = () => {...blade, ...handle}export default Knife;// ^ ✅ Valid// For completeness:export default class anyoneStillUseThese {}// ^ ✅ Also valid to export a class as default
-
Summary
There are actually many combinations of ways we could export things, each tells a different story:
| Default (Exports) | Named (Exports) | Private Fns | Pattern | Meaning |
|---|---|---|---|---|
| ✅ | ❌ | ❌ | One default export. | “Presenting ONE function w/ Single Purpose!” |
| ❌ | ✅ | ❌ | One named export. | “Please don’t rename me.” |
| ✅ | ✅ | ✅ | Default export + multiple ‘private’ un-exported functions | “Here’s some related logic. Also, expect class-ish behavior.“ |
| ❌ | ❌ | ✅ | Multiple named exports, generic file name. | “A grab-bag of loosely related things, no hierarchy implied.” |
| ✅ | ✅ | ❌ | Single named export ALSO exported as default. | “You can’t mess up importing me.” |
Something to think about: What are we saying when the file name does or doesn’t match one of its exports? (For example, a utils.js with many functions.)
Conclusion
If Code is Communication, please export like you fucking mean it. 💞



