The barrel exports pattern is described in Basaratâs book (TypeScript Deep Dive), and looks like this:
/// index.ts
export * from "./a";
export * from "./b";
// [¡¡¡]
export * from "./z";
Itâs pretty common, most people use it, and I donât blame them: itâs actually very useful. It saves us from having to remember too many import paths, and from having to explicitly export every symbol from our submodules inside the package.
So⌠yes, I admit it, the title of this article was pure click-bait. I donât think this pattern is always harmful, only sometimes. When and why, then?
Itâs almost always about coupling
Side effects at module load time
Although not likely to happen (because most of us have learnt the hard way that itâs a bad idea to perform side effects at module load time), thatâs the simplest problematic case we can explain.
When we âneedâ to perform some side effects at load time (whatever the reason: singletons, resource initialization, monkey patchingâŚ) in one of the submodules that is star-exported on our âbarrelâ, weâll be unable to avoid them when importing anything that is completely unrelated from that same barrel.
As an example, if we just wanted to import a utility function, we might be triggering some resource initialization that had no relation to that task (affecting performance for no good reason), or performing unintentional monkey patching, leading to potential hard-to-debug problems.
/// lib/index.ts
export * from "./a";
export * from "./b";
export * from "./c";
/// lib/a.ts
export const greet = (name = "stranger") => console.log(`Hello ${name}!`);
console.log("Loading module a"); // Side effect
/// lib/b.ts
export const square = (x: number) => x * x;
console.log("Loading module b"); // Side effect
/// lib/c.ts
export const cube = (x: number) => x * x * x;
console.log("Loading module c"); // Side effect
/// main.ts
import { greet } from "./lib";
greet("World");
/// main.js's output:
// --------------------------------------------------------------------
// Loading module a
// Loading module b // <- We didn't want this
// Loading module c // <- We didn't want this
// Hello World!
âType effectsâ at type checking time
You might be thinking that the previous case is not really that problematic, because no side effects are performed in most code you see every day (consider yourself lucky!), but thatâs not where the story ends.
First of all, Iâm probably going to abuse the term âtype effectâ on the following lines (I suspect this term already has some different meaning attached, but given that I donât have a better word, Iâll stick to it for now), I ask for your forgiveness in advance⌠and if you know of a better way to describe it, please feel free to reach out to me and tell me!
While most of the times types have to be explicitly imported in order to have any effect at type-checking time, there are some interesting cases that donât work like that, module augmentation and ambient modules (both rely on the same construct âdeclare moduleâ):
declare module "some_external_module" {
// typing stuff in here
}
What this does is to overwrite or augment the types exposed by the pointed module, and can be used (for example) when relying on auto-generated code. One interesting case of this is GraphQL to TypeScript code generation, and how this is integrated with the amazing Mercurius library (made by some of my colleagues at NearForm! đ).
Itâs not a coincidence that I mention auto-generated code and Mercurius: I actually found myself having to deal with a barrel that was exporting one of these auto-generated files. That barrel was inside a supposedly generic types library (in the context of a monorepo)⌠and because of it, it was âleakingâ types from one federated âgatewayâ into another one that was supposed to expose a completely unrelated object graph. Untangling that wasnât fun.
Generated code can also suffer
When some of our modules only contain types (but no runtime symbols), we probably want that the generated code doesnât really perform a runtime import because itâs totally unnecessary.
If we use import type
instead of just import
, or our import statement is explicitly importing something that itâs just a type without a runtime counterpart (basically, not enum
nor class
), then we can expect tsc
to optimize away that import from the generated JS code.
Sadly, tsc
is not that smart when it comes to âstart-exportsâ, and we wonât be able to optimize it away. It is true that the compiler can improve in the future, but I wouldnât hold my breath for now.
Indirect problems
The barrel pattern makes it easier to introduce accidental circular references. It is true that the fault in this case doesnât really fall on the pattern, but we should prefer to not have footguns at our disposal.
Iâve seen too many times how some people decide to import âsymbolsâ from âsiblingâ modules through the index.ts
file instead of directly addressing the source modules; the error here is not the barrel itself (because itâs thought to be consumed from âoutsideâ), but consuming âinternalâ code as if it was an external library.
Conclusion
Barrel exports are a great tool, if used correctly, but thereâs great risk of abusing them. As with any other technique, we should be aware of its associated trade-offs and under which circumstances they can become problematic, so we can consider these factors when deciding whether to use them or not.
Also published here.