Modules Confusion: Default Exports & Named Exports

Modules have two kinds of exports - named exports and default exports. Named exports allow a module to export multiple bindings under different names, and consumers can import some or all of these. Default exports are a convenience for consumers - rather than requiring them to know the module name and the member name they need only remember the module name.

Based on initial feedback, this design is difficult to explain and difficult to understand. Further, it seems to offer little significant value to developers today. Simplifying and aligning the system more closely to existing module systems by only allowing either a default export or many named exports would address a significant point of confusion.

Consider History

The conceptual model that people have for modules today is based on existing module systems. This will impact how ES6 modules are used. It is possible that many people will not use named exports at all and simply export a default object with properties for a few reasons. First, it's likely that this will be the easiest path - jQuery for example can simply export default $. Just using a default also makes it unambiguous how to consume - use import. It also aligns with expectations based on existing module systems.

The ES6 Modules proposal departs from this conceptual model, but doesn't need to in order to accomplish its goals.

Consider JQuery

Consider a simplified version of jquery - it has a function, traditionally named $, that users either call (ie. $('#foo')) or dot off of to get additional functionality (ie. $.get(...)). The obvious choice for the ES6 JQuery module is to export $ as a default export.

// jquery.js 
function $() { };
$.get = function() { };
$.post = function() { };
export default $;
 
// user code 
import $ from "jquery.js";
$.get('/foo.json').then(...);

Works great, as it does today. However, JQuery may want to allow users to import helpers directly for cases where they don't care to use $ but do want to use ajax helpers. So, quick modification:

// jquery.js 
function $() { };
$.get = function() { };
$.post = function() { };
export default $;
 
export { $.get as get, $.post as post };
 
// user code 
import { get } from "jquery.js";
get('/foo.json').then(...);

Possible Confusion

The example above illustrates a problem - how should jQuery be consumed? Specifically, all of the following seem equivalent:

import $ from "jQuery";
var get = $.get;
 
// vs. 
import { get } from "jQuery";
 
// vs. 
module $ from "jQuery";
var get = $.get;

In many conversations I've had about modules I've had to explain the distinction between module and import. From a syntax standpoint, the semantics of module and import seem arbitrary and could be easily exchanged. Further, in the example of jQuery above, it's not clear how users should consume jQuery - there are multiple ways to get at ajax. Neither method seems to offer much value over the other methods.

Additionally, there is no way to know how a module wants to be consumed without consulting the module code or documentation. This is certainly the case today as well (some prefer to expose a "default export" while others prefer to export an object with properties) but the current proposal adds more dimensions (ie. the three options for jQuery above).

Consider Math

For modules like Math, where users will likely want to import all the named exports to use as needed. In this case, the module producer has two non-mutually-exclusive options - create a bunch of named exports, export a default object that has many properties, or both. Depending on the choice of the producer, the consumer must use different import syntax. It is not clear to the user what kind of module they are trying to import and so they will have no way of knowing how that module wants to be consumed without consulting the code or documentation.

Consider Dynamic Cases

JavaScript has a long and storied history of "crazy" dynamic code patterns. The current module design runs counter to this. Consider a scenario where the developer wants to import some arbitrary module and re-export some modifications. For example, perhaps the developer wants to wrap modules and only re-export the bindings listed in a config file. The only way to accomplish this today is to expando the default export, but unfortunately this does not allow consumers to import specific named bindings.