Broken `Export Default`


Javascript Modules Crash Course

There are a couple of ways to define and require modules in Javascript.

First, CommonJS:

const utils = require("src/utils");
const translate = require("src/translate");

utils.log(translate("All set"));
module.export = {}; // exported value

The standard is implemented in node.

Second, AMD (Asynchronous Module Definition):

define(
  ["src/utils", "src/translate"],
  (utils, translate) => {
    utils.log(translate("All set"));
    return {}; // exported value
  },
)

The most popular implementation is Require.js.

Addy Osmani overviewed both in more detail.

The Good and the Bad

Original AMD syntax looks too verbose and it’s inconvenient to add/remove dependencies if you have a number of them.

That’s why Require.js supports CommonJS syntax since the first release. So everyone was using CommonJS before tc39 proposed new syntax in ES6.

ES6 Imports

Ecma 262 import spec

In 2015 we got a module system that looked exciting after all. I mean, AMD is totally weird, CommonJS allows dynamic and conditional imports as well as exports and uses globally defined module so none of them is good enough.

In my opinion, import statements should not look like an ordinary expression. It should not be allowed inside of a block, and it should give you more flexibility as qualified and named imports.

In ES6 we have all that and even more.

import React from "react";
import * as utils from "../utils";
import { foo } from "../foo";

According to the spec there are named and default exports. For example, if you have React+Redux app, you may want to export component as named member and connected component as default member.

export class FooCmp extends ...
// Named export
// import { FooCpm } from "..."

export default @connect() FooCmp
// Default export
// import FooCmp from "..."

The Ugly

However, ES6 imports are incompatible with CommonJS. Yes, the committee has accepted the module system for javascript that is incompatible with node.

It turns out there is no way to import default member of a node module because there is no such thing in CommonJS. Simply put, default export is a named export with a name default. And the line import React from "react" above fails in runtime unless package’s developer has exported everything under name default.

module.exports = { foo, bar }
module.exports.default = module.exports

Obviously, you can’t know that without looking into packages’ source code. So just in case, we have to use namespace import rather than default import * as React from "react" for packages.

However (again!), you may be using your own packages where ES6 modules with default exports are used. Which means you import differently depending on packages’ origin:

// that is CommonJS package, use qualified export
import * as React from "react";

// and that is my package
import Audio from "my-components/Audio";

Also, magic is gone when it comes to re-exporting ES6 modules. export * from "./Video" takes the ./Video module and exports everything from current module. Everything except default member, so you have to be explicit about your defaults:

export * from "./Video";
export { default } from "./Video";

Now Video’s default member became default member of the current module. Apparently, you can have only one default export per file. So to re-export several default members you would have to turn them into named ones:

export { default as Audio } from "./Audio";
export { default as Video } from "./Video";

Another example where we are hostages of unnecessary flexibility is our inability to rely on search. For example, if you import a too generally named module, say, utils, you may want give it move specific name. If you import from a module-with-dashes you can give it kebab-case or PascalCase name.

import leftPad from "left-pad"
import brangindUtils from "../branding/utils"

That is a problem because developers rely on code search a lot.

Let’s take another look at React. That’s what I found in Flow documentation.

Note: We import React as a namespace here with import * as React from 'react' instead of as a default with import React from 'react'. When importing React as an ES module, you may use either style, but importing as a namespace gives you access to React’s utility types.

They do have default export, but there is no way to understand what’s in here unless you have this red.

Most of the developers will never notice the problem because they use Babel which handles CommonJS modules with additional checks:

import * as React from "react";
import utils from "utilsWithDefaultExport";
export default utils.decorate(React);

// compiles to

Object.defineProperty(exports, "__esModule", {
  value: true
});
var React = _interopRequireWildcard(require("react"));
var _utilsWithDefaultExport = require("utilsWithDefaultExport");
var _utilsWithDefaultExport2 = _interopRequireDefault(_utilsWithDefaultExport);
// ...
exports.default = _utilsWithDefaultExport2.default.decorate(React);

On one side it is good, so you don’t have to think about it, but this behaviour is not compatible with the specification. On another side, you are in trouble once you switch to another transpiler.

On recap

Default export:

  • incompatible with CommonJS
  • makes your code entangled
  • breaks your search
  • syntax sugar with (almost) no benefit

That is a brilliant example of accidental complexity being added with no good motivation.

I wish I would have never used default exports. And apparently, I’m staying away from them.