Prefer Named Exports
· Last updated · 4 min read · #javascript #typescript

Prefer named exports.
Default exports feel lighter when a file has one main thing, and I used them for years. Then I renamed a component. The file name updated, the paths updated, and half the imports still called it by its old name. TypeScript didn’t mind at all, because a default import can be called whatever you like.
That’s the whole problem, and the rest of this post is the detail.
What is a named export?
A named export is a binding with a name, and you import it by that name, in braces.
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
import { add, subtract } from "./math.js";
You can still alias it, but the alias is written out where everyone can see it.
import { add as plus } from "./math.js";
A default export has no name at the import site. Each file that imports it makes one up.
export default function add(a, b) {
return a + b;
}
import add from "./math.js";
import sum from "./math.js";
import MathFn from "./math.js";
All three compile, and none of them has to match the function you actually wrote.
Why does that matter?
Say you default-export a component called ProductDrawer. A year later it’s a dialog, so you rename the function and the file to ProductDialog. The language service updates the import paths for you.
It doesn’t update this:
import ProductDrawer from "./ProductDialog.js";
There’s no error. Search the codebase for ProductDialog and you’ll miss every call site that still says ProductDrawer.
With a named export, the build fails until every import moves, or until someone writes import { ProductDialog as ProductDrawer } on purpose. Either way the mismatch is visible instead of silent.
It’s also why autocomplete works better. Type import { | } from a module and your editor lists its exports. A default import autocompletes the path and then lets you type any identifier you want.
Renames follow the same logic. Rename a named export in TypeScript and the language service can update every import. That’s the language service doing it, by the way, not ESLint; I’ve seen people credit the linter for this more than once.
Does this help tree-shaking?
Sometimes, but not in the way older explainers claimed.
Tree-shaking needs static import and export. webpack’s own guide (opens in a new tab) shows the case that actually breaks: a default-exported object full of helpers can’t be shaken apart.
export default {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
};
import math from "./math.js";
math.add(1, 2);
The bundler can see you using math, but it can’t prove nobody touches subtract, so both functions stay. Named exports give it one binding per function:
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
import { add } from "./math.js";
Now subtract can go.
A default-exported function is fine. If nothing imports the module, the whole module drops. So the myth is “named exports tree-shake and default exports don’t,” and the truth is “don’t default-export a namespace object.”
And remember that ESM itself doesn’t tree-shake anything at runtime. The bundler does.
When is a default export the right call?
When the host requires one.
Next.js pages and layouts (opens in a new tab) default-export the component for each route. File-based frameworks do this so there’s one obvious thing to render, and if you fight it you get The default export is not a React Component.
So do it there, and name the function anyway:
export default function Page() {
return <h1>Home</h1>;
}
The name still shows up in stack traces and in React’s component tree. The default is for the framework, and everything else in the file can be named.
Even a module that really is one thing still wants a named export if people import it. export function cn() beats an export default function that every caller names differently.
What should you not do?
Default-export an object of helpers. It’s a CommonJS habit. It defeats tree-shaking and it’s a worse version of import *.
Re-export defaults through a barrel without naming them. export { default as Button } from "./Button.js" is the least-bad form. export { default } from "./Button.js" just spreads the naming problem further.
Ban default exports with a lint rule, then spend a day fighting page.tsx. Turn the rule off for the files the framework owns.
I’d start with named exports everywhere in application code, and default-export only where a framework leaves you no choice.
Stay in touch
Don't miss out on new posts or project updates. Hit me up on X for updates, queries, or some good ol' tech talk.
Follow @zkmake