ESM import style for additional methods #6
Replies: 24 comments
-
Personally, I prefer the same syntax as you chose in the last code block. import log from 'log';
log('a');
log.stderr('b'); I only use named exports when the name very clearly communicates the context and purpose, otherwise it may cause confusion, especially when using too generic names. Mixing default and named exports is another thing I'd avoid, I'd either go with one or the other, but not both. |
Beta Was this translation helpful? Give feedback.
-
Assuming that export default function log() {}
log.stderr = () => {} With that can tools three shake the module? I don't think so but I may be wrong. |
Beta Was this translation helpful? Give feedback.
-
@giuseppeg Tree-shaking is not a concern here. It's for Node.js. But no, I don't think that would be tree-shakeable. |
Beta Was this translation helpful? Give feedback.
-
I'm personally not a fan of default exports as this leads to naming inconsistencies as well as worse tooling support (VS Code or WebStorm can autocomplete named exports, but not default ones, assuming these are anonymous). I therefore prefer the case where both are named. You could make both worlds happy and write: export function log() {};
export function stderr() {};
export default log; |
Beta Was this translation helpful? Give feedback.
-
@sindresorhus oh right, then I like your/@wolfika's preference |
Beta Was this translation helpful? Give feedback.
-
@thomaux Your issue can be fixed by tooling though. VS Code could just auto-complete the default export to be a camel cased version of the package name.
That's IMHO the worst of both worlds:
|
Beta Was this translation helpful? Give feedback.
-
VS Code can apparently autocomplete named default exports, so my point is specifically against using unnamed default exports. |
Beta Was this translation helpful? Give feedback.
-
This is the direct equivalent: export default function log() {}
log.stderr = () => {}; import log from '.';
log()
log.stderr() You export a |
Beta Was this translation helpful? Give feedback.
-
The closest alternative is export function log() {};
export function stderr() {}; import * as log from '.';
log.log();
log.stderr(); |
Beta Was this translation helpful? Give feedback.
-
How about this case: https://github.com/sindresorhus/read-pkg/pull/8/files#diff-b52768974e6bc0faccb7d4b75b162c99R29 const readPkg = require('read-pkg');
readPkg();
readPkg.sync(); Here they are really distinct methods. Almost seems like this would be best? import {readPkg, readPkgSync} from 'read-pkg';
readPkg();
readPkgSync(); |
Beta Was this translation helpful? Give feedback.
-
@sindresorhus This is the best. import {readPkg, readPkgSync} from 'read-pkg';
readPkg();
readPkgSync(); Default export is the source of problems quite often. |
Beta Was this translation helpful? Give feedback.
-
es6 named imports are mainly for tree shaking so for node you don't really need them. because of this it's best to focus on ease of use rather than proper splitting into named exports. My advise is to use import readPkg from 'read-pkg'
readPkg()
readPkg.sync() because it mimicks commonjs interface, is easier to migrate to, and you don't need to write separate documentation for es6 modules and commonjs modules version.. EDIT: Also you don't need to remember both package name and names of named exports, not everybody is using IDEs. Also you don't need to use EDIT2: I also think introducing ES6 analogous to CommonJS means that you don't need to bump major version of such package because the usage is the same and it's safe for everyone to enable e.g. webpack to automatically pick ES6 files instead of CommonJS. It's because most bundlers allow to use EDIT3: Of course my argument would be totally different for universal npm package because then granularity of named exports really matters because of tree shaking. In such case it's best to release major version of package with changed interface that leverages named exports. |
Beta Was this translation helpful? Give feedback.
-
I’m starting to not love “default” exports even in CJS, since it’s pretty painless to dereference properties. Tooling seems to have a better handle on it, but mainly I just like the consistency. I like this: import {log, logStderr} from 'log';
log('a');
logStderr('b'); In CJS that could be const {log, logStderr} = require(‘log’) |
Beta Was this translation helpful? Give feedback.
-
You'd be surprised. If you add an ES Module with a |
Beta Was this translation helpful? Give feedback.
-
Node.js should still care about tree-shakeability. The last code snippet is great if it's purposefully chosen from a place of API design - but choosing it on the basis of apathy is problematic IMO. ESM is basically meant for tree-shaking. |
Beta Was this translation helpful? Give feedback.
-
I went through a similar process. In fact, I've gone back and forth, but here's hoping my answer will stand the test of time. Like many others, I've basically decided that default exports were a mistake. I therefore try my best to only use named exports now. Additionally, for the stderr case, put me in the Not only does this help with tree shaking (even though that's not a concern here), but it also clarifies the API of a newly introduced module. I've too often installed an npm package and spent precious time figuring out whether its (default) export was a class, a function, an object, or whatever, and what I was supposed to call it as a result. It makes more sense to leave that decision up to the module maintainer because they're the domain expert. |
Beta Was this translation helpful? Give feedback.
-
Doesn't sound like an argument against default exports. If you don't know what type it is, the documentation failed you. It could happen for named exports as well. I think the current issue with default exports is the tooling and the mismatch between CJS default exports and ES Modules For cross-compatibility I've resolved to only publish CJS modules if I want to export one thing, even if that one thing is exactly what Sindre is describing. Example: https://github.com/bfred-it/select-dom#readme |
Beta Was this translation helpful? Give feedback.
-
I would agree with you if my editor didn't tell me the names of the exports, meaning I don't necessarily need to look at the documentation (at first). If I see an export called
Not to advocate for premature optimization, but I know for a fact that a lot of Sindre's modules initially had one export. For example, I contributed the initial implementation of the The same has been true for many packages and modules that I've written or worked on myself. Named exports give you a nice incremental path, while a default export can really end up biting you. Additionally, it's a matter of consistency. If I know that all my (or our company's) packages and modules only used named exports, I don't need to check each module's documentation to know about the employed style. |
Beta Was this translation helpful? Give feedback.
-
I don't think this a good example to prove your point, because |
Beta Was this translation helpful? Give feedback.
-
Like I said, personally, I really don't like this mix of two styles. Aside from the fact that the import is harder to read, it's very unpredictable and I hear it makes interop quite hard. Additionally, I'm not convinced that the default export is always going to be that clear. Take the example of sync vs. async with CPS. Do you then make the CPS version the default export? And what if you add a promise-based version later? |
Beta Was this translation helpful? Give feedback.
-
I don't like the default export because there's usually more than one export. I don't mind generic named exports because they belong to the module I'm exporting from and can easily rename them. I run a lot of code in lambda these days where bundle size does matter so treeshaking is a concern. (Previously I ran a lot of code on small embeded devices, where bundle size also mattered) The best of both worlds seems to be named and default. import log, {stderr as logStderr, log as exportedLog } from 'log'; There is more than one way to get
|
Beta Was this translation helpful? Give feedback.
-
Linting could be a reason to prefer named exports over default exports. Default exports do not help catch copy-pasta, for example: import BaseClass from './child-class1';
import ChildClass1 from './child-class1';
import ChildClass2 from './child-class2'; In this example the In most cases actual tests should pick up this kind of error but named export would allow the linter to give a more direct error message than whatever failure is produced by tests. |
Beta Was this translation helpful? Give feedback.
-
@coreyfarrell A linter rule could detect that case though. The rule could be that the default import name should be a camelcased or pascalcased version of the import specifier ( |
Beta Was this translation helpful? Give feedback.
-
I know this is an older discussion but I came across 16. Modules in Exploring ES6: Upgrade to the next version of JavaScript and it contains the following pointing out that the syntax is designed to favor default imports:
|
Beta Was this translation helpful? Give feedback.
-
I have this module. Let's call it
log
. Currently, in CommonJS, it exports a function for logging to stdout. The function also has a property calledstderr
that is a function that logs to.stderr
instead:What's the best way to translate this to ESM?
Most people would probably go with:
But I find the
stderr
naming too generic.I could rename it:
But that's verbose and ugly.
I could also make both named exports:
But I prefer default exports and would like to optimize for
log
, as it's what most would use.Personally, I don't think we should use named export for things that are tightly coupled to the default export.
I would go with:
Thoughts on going with this syntax?
Some additional discussion on Twitter: https://twitter.com/sindresorhus/status/1102856012103475200
Beta Was this translation helpful? Give feedback.
All reactions