As you may already know, there are two different ways to share functions in JavaScript: default exports and named exports. Default exports are used to export a single value from a module, while named exports allow you to export multiple values from a module.
Default exports are great when you want to export something specific from a module, like a function or a class. Named exports, on the other hand, are perfect for when you want to export multiple things from a module at once.
In this post, we'll explore why using default exports isn't recommended and should be avoided. To get started, let's take a look at some examples of both.
Named exports
An example of a named export is as follows:
// add.ts
export const add = (a, b) => a + b;
In this example, we've exported a function called add
from the add.ts
module. You can easily import this function into another module using its name:
// app.ts
import { add } from './add';
console.log(add(2, 3)); // 5
Default exports
Let's rewrite the example above with using a default export :
// add.ts
const add = (a, b) => a + b;
export default add;
In this example, we have exported the add
function as the default export. This function can then be imported in another module using any name of our choice.
// app.ts
import add from './add';
console.log(add(2, 3)); // 5
Now that you have a good understanding of named and default exports, let's dive into the next section to explore the disadvantages of using default exports.
Poor discovery
Default exports can be confusing in complex modules. Developers may not know which export is the default or even be aware of its existence. This can cause delays as engineers must spend more time looking at documentation or even the source code to find the functions they need.
On the other hand, using named exports makes discovery much simpler. With named exports, developers can easily see the exported members of a module and their corresponding names. This is especially helpful when using an IDE, as popular IDEs allow you to use a shortcut (such as cmd + space
) to suggest available functions from a given file. Unfortunately, this shortcut doesn't work when using default exports.
Refactoring
If you decide to rename a named export in a module, for example, changing the name of the add
function to sum
, most IDEs can automatically update all usages. This makes the refactoring process much easier.
However, with default exports, this isn't possible.
Auto-complete
As we discussed earlier, when a module provides named exports, we can easily select a specific function from the module by using a shortcut provided by the IDE. This feature not only saves time, but also helps the IDE suggest and auto-import the necessary functions as we type.
For example, if you start typing add
, the IDE will display a list of available packages that provide the add
function. All you have to do is choose the right package from the list, and the editor will automatically insert the import for you. It's that simple!
Inconsistent codebases
Default exports can be imported using any name you choose. This can lead to inconsistencies when multiple engineers are working on the same codebase and using different import names.
import add from './add';
// Other engineer could use another name
import sum from './add';
Re-exporting
When developing an npm package, it's common practice to export the package functions in its entry point, which is typically named index.js
or index.ts
if the package is implemented in TypeScript.
To keep things organized, we often create separate files for different functions and then re-export them in the entry point file. For example, we might have files named add.ts
and multiply.ts
, each containing a function to add or multiply two numbers.
If we use default exports for those files, we need to specify the names of the functions that will be available in the final package.
// index.ts
export { default as add } from './add';
export { default as multiply } from './multiply';
Using named exports is more convenient:
// index.ts
export { add } from './add';
export { multiply } from './multiply';
What if you want to export everything? That's where wildcard exports come in – they make it even easier.
// index.ts
export from './add';
export from './multiply';
Conclusion
Using default exports can make it harder to find things in your code and lead to inconsistencies. Instead, named exports make it easier to discover which members are being exported and what their corresponding names are, especially when using an IDE.
It's recommended to avoid using default exports and instead opt for named exports whenever possible. By doing so, you'll have a more organized and maintainable codebase that's easier to work with in the long run.
If you found this series helpful, please consider giving the repository a star on GitHub or sharing the post on your favorite social networks 😍. Your support would mean a lot to me!
If you want more helpful content like this, feel free to follow me:
Top comments (27)
Actually IDEs should not have any problem finding your default exports aslong as they are not anonymous functions or objects. If you use recommended eslint rules, it will tell you not to export anonymous functions and objects.
Yeah but that's a "if you setup that stuff correctly" 😕
Using named exports doesn't depend on ideal circumstances
This works in vscode without any additional configuration and I'm pretty confident it works in most other editors as well, if they understand named exports they also understand default exports as long as your default exports are not anonymous functions.
There is a clear difference between function expression and function declaration and they both serve a purpose.
This can't be a viable solution since imposing function declaration would mean losing the benefits of arrow functions that grab the outer context instead of creating a context. This can be destructive for some project relying on this mecanism.
Also, you can't use that reliably on scalar data types that uses default exports, it simply fails short on your text editor and its completion system (which comes most of the time from the language server). TypeScript/JavaScript language servers do not work well on default exporting scalar data types. So using a default export is very much problematic for a lot of things.
Instead, named exports are harmless in the sense that you can either named export a function expression as well as a function declaration, it does not impose on your team a way of doing things, plus the benefits mentioned above in this article. And it provides the best completions and auto-import capabilities from your language server when used in your text editor (and many editor benefit from that such as Emacs, Neovim, IntelliJ, VSCode, etc...).
You can do this with arrow functions as well:
I've read this paragraph several times now and I still think it makes no sense whatsoever. Can you explain what you mean by this?
Again, this doesn't make any sense to me whatsoever; what is the problem here? Default exports have no name, so what would you want to happen here?
This one makes more sense, but it's not really an argument against default imports. As long as your default export is also available as a named export (which is probably a good idea regardless), this still works 100%.
Again, this is a good reason to also have a named export for your default export, but it doesn't speak against having a default export in any way.
Setting that aside, regardless of consistency or not, if I look at a source file and see
import add from "sum"
I will probably scratch my head multiple times and tell whomever wrote that to fix it (or just not interact with the project if it's OSS)Conclusion
Just provide named alternatives for your default export and everyone will be happy. We can have both.
Isn't that a bit repetitive though? Regarding both default and named exporting.
it's three keywords per file, I don't think that's the kind of thing to seriously be worried about.
This could be a discussion rather than a judgement, this way you're misleading others, named export or default export each export type has a purpose, some times you need to export a single function from a file to serve specific purpose, as you can use named export when you wants to group multiple functions in a single file etc
You had me at "Refactoring" - very true.
Yep, spot on. Especially the Inconsistent Codebases section.
In the rare instances where a named export does need to be renamed, it can always be done with
as
:I'd say what the OP is suggesting is probably the safer bet, but this really is only an issue in really big code bases with lots of people working on it. Also in frameworks like React that have strict file naming conventions as long as you follow those conventions, you should just use the previous standards not make up your own. Another situation where default exports are better is when you are making a npm package, in this use case it only makes things worse for the user if they have to remember the name of the export.
Most of these aren't real problems, and many people use default exports just fine.
I have worked on code bases with default exports where casing and typos have caused discoverability issues.
If it can break "find in files" that's a good enough reason for me to not use it. IDEs make our lives easier in a lot of ways, but at the end of the day an IDE is just a tool. If you need it to understand your code, there's a problem. Anything that makes the code easier to understand without tooling is best practice in my book.
Default exports are not needed. Exporting a single named function from a module is completely unrestricted. If you need to rename a module, use the
as
directive.Then grepping the codebase will at least see the import reference. You won't even need it if you take your time namig things and refactor when things become so similar that their names overlap. So why use default exports?
Good thoughts, I like that you're challenging convention and asking us to think through things rather than just following the norms.
In my opinion, all of what you're saying is true when a file has multiple exports. I do find it confusing to have a default export mixed with named exports. But typically when a default export is used, I only see a single export from that file. I haven't run into any problems with that, especially since the file, function, and import almost always share the same name.
But at the same time, I also see no argument against using only named exports, so maybe you're on to something.
The IDE has no issues finding default exports while typing. It is a matter of providing proper information. Someone already said that if you export a nameless something, then that's a problem, but if you export something with a name, that name shows up in autocomplete.
Furthermore, NPM packages should provide proper d.ts files. Using my
vite-plugin-single-spa
package as example, you just install, then openvite.config.ts
and you start typing "import vitep". At this point you should see autocomplete showing you the right option (vitePluginSingleSpa
).To me , it seems like this articles seems like a personal issue l.
I use Vue , and Typescript , on top of that I apply OOP , which means there are a lot Class files that it makes sense that one File holds 1 default class and maybe some types. I use default export often an I have never any of the problems you mentioned my IDE (Webstorm, intelij) knows exactly where are they. On top of that I use the default export in conjunction with "named export" andy IDE k ow exactly how to import them . import A, {b}.