4

What are the consequences of exporting functions from a module like this:

const foo = () => {
  console.log('foo')
}

const bar = () => {
  console.log('bar')
}

const internalFunc = () => {
  console.log('internal function not exported')
}

export const FooBarService = {
  foo, 
  bar
}

The only reason I've found is that it prevents module bundlers to perform tree-shaking of the exports.

However, exporting a module this way provides a few nice benefits like easy unit test mocking:

// No need for jest.mock('./module')
// Easy to mock single function from module
FooBarService.foo = jest.fn().mockReturnValue('mock')

Another benefit is that it allows context to where the module is used (Simply "find all references" on FooBarService)

A slightly opiniated benefit is that when reading consumer code, you can instantly see where the function comes from, due to that it is preprended with FooBarService..

You can get similar effect by using import * as FooBarService from './module', but then the name of the service is not enforced, and could differ among the consumers.

So for the sake of argument, let's say I am not to concerned with the lack of tree-shaking. All code is used somewhere in the app anyway and we do not do any code-splitting. Why should I not export my modules this way?

micnil
  • 4,705
  • 2
  • 28
  • 39

3 Answers3

2

Here are a couple of other benefits to multiple named exports (besides tree-shaking):

  • Circular dependencies are only possible when exports happen throughout the module. When all exporting happens at the end, then you can't have circular dependencies (which should be avoided anyways)
  • Sometimes it's nice to only import specific values from a module, instead of the entire module at once. This is especially true when importing constants, exception classes, etc. Sometimes it can be nice to just extract a couple of functions too, especially when those functions get used often. multiple named exports make this a little easier to do.
  • It's a more common way to do things, so if you're making an external API for others to consume, I would just stick with multiple named exports.
  • There may be other reasons - I can't think of anything else, but other answers might mention some.

With all of that being said, you'll notice that none of these arguments are very strong. So, if you find value in exporting an object literally everywhere, go for it!

Scotty Jamison
  • 10,498
  • 2
  • 24
  • 30
  • 1
    1. Did not know that it can have an affect on circular dependencies, thanks! 2. Valid point, I guess it is not appropriate to use this pattern for all modules. 3. This is also my understanding. Not common pattern, which make's you think "yuck" in the beginning ^^. Although, it is not an external API, atleast not a public external API (only within our org) – micnil May 29 '21 at 17:59
2

Benefits of using individual named exports are:

  • they're more concise to declare, and let you quickly discover right at their definition whether something is exported or not (in the form export const … = … or export function …(…) { … }).
  • they give the consumer of the module the choice to import them individually (for concise usage) or in a namespace. Enforcing the use of a namespace is rare (and could be solved with a linter rule).
  • they are immutable. You mention the benefit of mutable objects (easier mocking), but at the same time this makes it easier to accidentally (or deliberately) overwrite them, which causes hard-to-find bugs or at least makes reasoning harder
  • they are proper aliases, with hoisting across module boundaries. This is useful in certain circular-dependency scenarios (which should be few, but still). Also it allows "find all references" on individual exports.
  • (you already mentioned tree shaking, which kinda relies on the above properties)
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • These are all good points. I will note, if immutability is a strong concern, you can use Object.freeze() to export an immutable object. – Scotty Jamison May 29 '21 at 17:55
  • Good point with the flip side of having a mutable export. @ScottyJamison I guess using Object.freeze() would prevent the benefit of easy mocking as well..? – micnil May 29 '21 at 18:02
  • 1
    @micnil Yes, you can't mock properties of a frozen object. – Bergi May 29 '21 at 18:04
1

One potential issue is that it'll expose everything, even if some functions are intended for use only inside the module, and nowhere else. For example, if you have

// service.js
const foo = () => {
    console.log('foo')
}
const bar = () => {
    console.log('bar')
}
const serviceInternalFn = () => {
    // do stuff
}

export const FooBarService = {
    foo,
    bar,
    serviceInternalFn,
}

then consumers will see that serviceInternalFn is exported, and may well try to use it, even if you aren't intending them to.

For somewhat similar reasons, when writing a library without modules, you usually don't want to do something like

<script>
const doSomething = () => {
  // ...
};
const doSomethingInternal = () => {
  // ...
};
window.myLibrary = {
  doSomething
};
</script>

because then anything will be able to use doSomethingInternal, which may not be intended by the script-writer and might cause bugs or errors.

Rather, you'd want to deliberately expose only what is intended to be public:

<script>
window.myLibrary = (() => {
    const doSomething = () => {
        // ...
    };
    const doSomethingInternal = () => {
        // ...
    };
    return {
        doSomething
    };
})();
</script>
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • 3
    I see your point, but I guess if it's an internal function to the module, I wouldn't put it in the exported `FooBarService` object. Just like I wouldn't add the `export` keyword on it if I did exported the "regular" way – micnil May 29 '21 at 17:25
  • 1
    Well, you did say *exporting an object of all functions from a module*, not *exporting an object of functions intended for external use from a module* - and the latter would make perfect sense, since if you didn't export something intended for external use, it wouldn't be usable. – CertainPerformance May 29 '21 at 17:27
  • I see my mistake, badly formulated question, will update – micnil May 29 '21 at 17:28