Skip to content

Change: Prefer default export to no default export

Change pattern proposal: prefer default export to no default export

Old Pattern

The current setup uses import/prefer-default-export to enforce a default (versus named) export when only one thing is exported from a module.

New Pattern

I am suggesting that we invert this requirement and prefer (or require) named exports from all modules, regardless of how many exports they have.

"rules": {
    "import/prefer-default-export": "off",
    "import/no-default-export": "error"
}

Advantages of switching patterns

I have fundamentally drawn most of my insipiration from this post by Nicholas Zakas, so I highly recommend reading it.

  1. The names of things are consistent throughout the code.
    What something is called one place is how it's referenced another place.
  2. Adding an export to a module will no longer lead to awkward mixes of named and default exports.
    Or - worse - having to significantly refactor large portions of the codebase to remove a default export and replace it with the new named export. All modules will always export named exports, meaning a future change will not affect the existing code.
  3. Names will always be explicit and - if used - import aliases will be very clearly marked (with as, in this case).
    I generally believe that explicit is far better than implicit, especially when it comes to module interfaces.
  4. We can leverage native language early-throws and editor tooling to give better hints.
    Default exports don't throw an error if not present until the consumer tries to use undefined in whatever context they expected to receive something. Editor tooling can inspect files and directories to expose named exports, automatically update imports, and more. Presuming excellent internal names, this is effectively a self-documenting API exposed via named exports.
  5. There will not be a decision to make between single-export and multiple-export modules.
    All modules will export named exports, which means less cognitive overhead. Moving from file to file will no longer mean needing to understand the entire file to determine if there should be only one or multiple exports. Each export can stand independent of others.
  6. There may be a small tree-shaking benefit in any cases where a default export is used where there could be multiple named exports. I'm not sure how frequently this case occurs, but it's probably not much.

Disadvantages of switching patterns

  1. Aliasing imports will require more code (import { theRealName as myLocalName } versus import myLocalName)
  2. This pattern may be uncomfortable for those highly attuned to CommonJS-style modules
  3. This is a direct reversal of a current linting rule, which means a 180° mental flip for everyone comfortable with the current styles
  4. It could result in exported names that don't carry much or any meaning with them.

What is the impact on our existing codebase?

As recommended, this would have a pretty huge impact on our codebase. As of this writing, there are 1865 export default in EE - to say nothing of the many imports (which are much more complicated to modify wholesale due to not having PRO#1 above). Some of the exports are extremely easy to update (e.g. export default class Activities => export class Activities), but others would be much more manual (e.g. export default {... => export const thoughtfulName = {...).

As an intermediate solution, the no-default-export could simply be a warning, so there would be no nudge toward a default, and there would be a strong nudge to use named exports.


Note: I've marked this as a two-way door decision, because it's technically a reversible change. That said, very wide-reaching changes like this are a pain, so reverting this change after starting down this road would likely be very groan-inducing among everyone.

Edited by Thomas Randolph