Let’s say we want to decouple something - some utilities, for example - into a separate file.
If we have a main module called myModule.js
, we can create a file named myModule.utils.js
in the same directory.
Inside this new file, we might write something like:
export const doSomething = () => {...}
export const doSomethingDifferent = () => {...}
and so on.
The problem with this approach is that all these functions are now available for import throughout the entire codebase - even if they’re meant to be internal.
You could argue: "Well, then just don’t decouple them - keep them with the main module". But that’s not always practical. There are plenty of cases where splitting out a relatively large module makes sense, even if we don’t want those parts to be globally accessible.
This problem becomes especially apparent when relying on "autocompletion + auto-import" features in your IDE. If you have 20 functions named doSomethings
across your codebase, auto-import becomes difficult to use effectively.
One workaround is to do something like this:
export default {
doSomething,
doSomethingDifferent,
};
This disguises the problem a bit but doesn’t really solve it. You reduce the autocomplete clutter outside the module, but at the cost of making things harder to work with inside the module, potentially affecting tree-shaking, and still not preventing imports from outside - it just hides them from suggestions.
Also, in TypeScript, we can’t do something like:
export default {
MyInterface,
}
So for types, the closest alternative to export default
is namespacing:
export namespace MyModuleTypes {
export interface MyInterface {}
}
But namespaces aren’t really recommended for new projects, as we’re generally moving toward ES modules.
Just to clarify - I fully understand that TypeScript is a separate topic here, but I want to highlight the issue from different perspectives. I believe that with a proper solution, we could address both JavaScript entities and, potentially, TypeScript types in a unified way.
I’m sure there are multiple ways to solve this problem - some requiring more effort, some less.
Below I will consider a couple of options of how, in my opinion, this could look in JavaScript.
Option 1 (Probably the simplest implementation):
We already have directive support in JavaScript - for example, 'use strict'
.
Next.js uses 'use client'
for their purposes.
Maybe we could introduce something like 'use internal'
at the top of a file, meaning everything exported from this file is intended to be imported only within the current directory (and its children), but not from outside.
Build tools could support this. IDEs could support this. Linters could support this.
Option 2 (A more advanced solution):
A more advanced solution could involve adding an internal
modifier to the export syntax.
For example:
export internal const doSomething = () => {...};
This might be more difficult to implement, but it would also be much more powerful and flexible in the long run. With this approach, we can control the level of availability for each export individually. We could also expand the list of accessibility modifiers if needed.
I’m not sure if anyone else shares my concerns about internal entities and their visibility in the global project scope. But if you have an opinion here - please join the discussion.