Default exports, without explicit syntactic support

# Axel Rauschmayer (11 years ago)

(I’m happy with David’s proposal, but I’d like to try bikeshedding one more time.)

Building on an idea by Kevin Smith: how about supporting default exports without any explicit syntactic constructs? The convention would be: the name "_" is used for default exports. The following is a single-export module.

// lib/MyClass.js - alternative A
export const _ = class {
    ...
};
// lib/MyClass.js - alternative B
export class _ {
    ...
}

// main.js
import { _ as MyClass } from "lib/MyClass";

Importing a multi-export module:

import fs from "fs";

fs.renameSync('foo.txt', 'bar.txt');
# Kevin Smith (11 years ago)

(I’m happy with David’s proposal, but I’d like to try bikeshedding one more time.)

For my part, I no longer think that the "import * as foo" change solves anything. The underlying problem is default exports - it's just plain confusing to users.

// main.js

import { _ as MyClass } from "lib/MyClass";


A similar idea was proposed by Andreas Rossberg last year, but it was shot down at the time. And honestly, I just don't think even a convention is necessary.

(This is going to be a bit long - bear with me.)

Let's take a step back for a second. The "export overriding" thing (i.e. module.exports = ?) was never a part of the CommonJS spec. So why did that pattern take off in Node?

Let's go back to CommonJS. If you had a module which just exported a single thing (which is obviously very common), then what would that look like? First let's look at the export side:

// my-class.js
exports.MyClass = function() {
    // initialize
};

exports.MyClass.prototype.method = function() { ... };

Now the import side:

// use-class.js
var MyClass = require("./my-class.js").MyClass;

Notice how we have to invoke three names on the import side: one for the variable name, one for the module name, and one for the exported name. This is an awful user experience. Now let's rewrite the same thing using "module.exports":

// my-class.js
function MyClass() { ... }
MyClass.prototype.method = function() { ... };
module.exports = MyClass;

// use-class.js
var MyClass = require("./my-class.js");

Ah - a much better user experience, right?

So do we need to mimic the same "export overwriting" in ES modules? Let's see:

// my-class.js
export class MyClass {
    constructor() { ... }
    method() { ... }
}

// use-class.js
import { MyClass } from "my-class.js";

What do you think? Is there any user experience issue here that needs to be sugared over?

Notice that, on the import side, exports overriding in Node is an equivalent user experience to normal importing in ES modules.

Therefore, my conclusion is that syntactic support for "default exports" does not provide any measurable improvement in user experience. On the other hand, default exports is clearly confusing to users. The balance seems clear to me: default exports needs to be dropped from the design.

Feedback? Counter-arguments?

# Marius Gundersen (11 years ago)

On Thu, Jun 26, 2014 at 3:39 PM, Kevin Smith <zenparsing at gmail.com> wrote:

The underlying problem is default exports - it's just plain confusing to users.

I agree

What do you think? Is there any user experience issue here that needs to

be sugared over?

Notice that, on the import side, exports overriding in Node is an equivalent user experience to normal importing in ES modules.

Therefore, my conclusion is that syntactic support for "default exports" does not provide any measurable improvement in user experience. On the other hand, default exports is clearly confusing to users. The balance seems clear to me: default exports needs to be dropped from the design.

Feedback? Counter-arguments?

I think this is a great suggestion.

The only thing I want to add that it should be possible to import the collection of exports (the bag) and named values from the bag:

import _, {map, reduce, filter} from "underscore";

assert(_.fiilter === filter);

Marius Gundersen

# Calvin Metcalf (11 years ago)

the slight difference in import { MyClass } from "my-class.js"; compared to var MyClass = require("./my-class.js"); is that in cjs MyClass is a variable the user create, meaning the user only need to know about 1 name from the remote module, the path, but in ES6 MyClass is set by the exporter meaning similar to var MyClass = require("./my-class.js").MyClass; I need to know both what the path is it's coming from and what the hell they named the export. In the case of a collision or just a badly named module you would be doing import { really_poorly_named_class_factory_bean as MyClass } from "my-class.js"; which is cognitively similar to var MyClass = require("./my-class.js").MyClass;

# Kevin Smith (11 years ago)

On Thu, Jun 26, 2014 at 9:56 AM, Calvin Metcalf <calvin.metcalf at gmail.com>

wrote:

the slight difference in import { MyClass } from "my-class.js"; compared to var MyClass = require("./my-class.js"); is that in cjs MyClass is a variable the user create, meaning the user only need to know about 1 name from the remote module, the path, but in ES6 MyClass is set by the exporter meaning similar to var MyClass = require("./my-class.js").MyClass; I need to know both what the path is it's coming from and what the hell they named the export. In the case of a collision or just a badly named module you would be doing import { really_poorly_named_class_factory_bean as MyClass } from "my-class.js"; which is cognitively similar to var MyClass = require("./my-class.js").MyClass;

Ah, right - thanks for mentioning this!

First - I reject the idea that making the export "anonymous" is in any way good practice. Frankly, naming things is essential to API design.

But default exports do give us a UX gain when we want to rename the import. The question is: is this a marginal gain, or an essential gain?

In my experience, I rarely need to rename an import: the ratio might be 10:1, at best. So my experience suggests that it's a marginal gain. Other experiences would be helpful.

If it's only a marginal gain, then I don't see how it can offset the confusion that default exports are causing.

# Kevin Smith (11 years ago)

First - I reject the idea that making the export "anonymous" is in any way good practice. Frankly, naming things is essential to API design.

Here's a concrete example of why you should always name your exports:

// A.js
export class A {}

// B.js
export class B {}

// main.js
export * from "A.js";
export * from "B.js";

If you rely on "anonymous" exports, you're going to be SOL when trying to aggregate like this.

# Kevin Smith (11 years ago)

If you rely on "anonymous" exports, you're going to be SOL when trying to aggregate like this.

Sorry, meant to add that I aggregate like this all the time.

# Axel Rauschmayer (11 years ago)
// my-class.js
export class MyClass {
    constructor() { ... }
    method() { ... }
}

// use-class.js
import { MyClass } from "my-class.js";

You do have redundancy in my-class.js and, as Marius pointed out, the importer has to know both the name of the module and the name of the entity inside the module. Not that big of a deal. Again, standardizing on _ for default exports helps, but then importing is more verbose:

// my-class.js
export class _ {
    constructor() { ... }
    method() { ... }
}

// use-class.js
import { _ as MyClass } from "my-class.js";
# John Barton (11 years ago)

On Thu, Jun 26, 2014 at 7:39 AM, Axel Rauschmayer <axel at rauschma.de> wrote:

// my-class.js
export class MyClass {
    constructor() { ... }
    method() { ... }
}

// use-class.js
import { MyClass } from "my-class.js";

You do have redundancy in my-class.js and, as Marius pointed out, the importer has to know both the name of the module and the name of the entity inside the module. Not that big of a deal.

But these examples are misleading. "my-class.js" is not the name of the module. Its is a module identifier, and probably incorrect since the identifier is both absolute and ending in .js.

A more realistic example is

import {MyClass} from "./pretentious/kernel/core/util/my-class";

In the big picture the time to type a few characters in regular pattern is completely overwhelmed by the time it takes to figure out what MyClass does and where it lives.

Again, standardizing on _ for default exports helps,

I guess you meant "developers may choose to adopt short export identifiers"; I don't suppose you are proposing to standardize _.

# C. Scott Ananian (11 years ago)

On Thu, Jun 26, 2014 at 9:39 AM, Kevin Smith <zenparsing at gmail.com> wrote:

(I’m happy with David’s proposal, but I’d like to try bikeshedding one

more time.)

For my part, I no longer think that the "import * as foo" change solves anything. The underlying problem is default exports - it's just plain confusing to users.

I am coming around to a similar, but I think not identical, conclusion. I like the syntax proposed in the other thread:

import foo, bar from "bat" as bat;

Now the confusion is between:

import _ from "underscore";
import "underscore" as _;

But here it's not "default exports" per se. I need to know whether the module defines a single named export, or whether I need to dereference from the module object. I can't know which to use without consulting the source of the module, and I expect that "real" ES6 code will probably end up with an unpleasant mix of the two styles of import.

I'm not sure that there is an easy solution here. The various compromises seem to try to make these two forms quasi-identical (modulo lazy binding, and given some support by the module author). Is that enough? Does it actually solve the problem?

In node modules the ambiguity is solved because (by and large) var Foo = require("foo").Foo; is just "not done". So in almost all cases I can just write var Foo = require("foo"); and assume that will be correct. But socially or syntactically deprecating one of our two ES6 import forms is problematic:

(a) We can't discourage the import _ from "underscore"; form because we are forced to use this form for, eg, for jquery. import "jquery" as $; won't work because $ needs to be a function object, not a module.

(b) We can't discourage the import "underscore" as _; form without giving up lazy binding.

There is a fundamental confusion here, but it's related mostly to the relative immutability of the "module" object and the mechanism for lazy binding. Default exports are one attempt to paper over these differences, but maybe not the only one.