What are Barrel Exports in JavaScript and TypeScript?
Introduction
If you've worked on a JavaScript or TypeScript project with multiple modules, you've likely encountered long and repetitive import statements. As projects grow, managing these imports becomes increasingly tedious. Barrel exports offer a pattern that helps keep your codebase clean and organized.
In this post, I'll explain what barrel exports are, how to create them, and when you should (or shouldn't) use them.
What is a Barrel Export?
A barrel is a file that re-exports modules from other files, consolidating them into a single entry point.
These files are typically named index.js or index.ts.
The term "barrel" comes from the idea of "rolling up" exports from multiple modules into one convenient location, like items stored in a barrel. The Angular team popularized this term in their style guides, and it has since become standard vocabulary across the JavaScript ecosystem.
Note
Barrel exports are a convention, not a language feature.
JavaScript and TypeScript don't treat index.js files specially at the language level.
The magic happens in module bundlers and Node.js's module resolution algorithm, which automatically look for index.js when you import a directory.
Here's what a project looks like before and after adding a barrel:
Before (no barrel):
src/
components/
Button.js
Card.js
Modal.js
Spinner.js
app.js ← imports from each file individuallyAfter (with barrel):
src/
components/
Button.js
Card.js
Modal.js
Spinner.js
index.js ← the barrel file
app.js ← imports from the directoryCreating a Barrel Export
Without a barrel, importing components requires specifying each file path explicitly:
import {Button} from './components/Button';
import {Card} from './components/Card';
import {Modal} from './components/Modal';
import {Spinner} from './components/Spinner';To create a barrel, add an index.js file in the components directory that re-exports everything:
export {Button} from './Button';
export {Card} from './Card';
export {Modal} from './Modal';
export {Spinner} from './Spinner';Now you can import everything from a single location:
import {Button, Card, Modal, Spinner} from './components';Real-World Examples
I use barrel exports extensively in my open-source projects. Here are some examples from real codebases.
YAKD - Kubernetes Dashboard
YAKD is a Kubernetes dashboard I maintain. The project uses barrel exports to organize its UI React components into a clean, importable structure.
The main components barrel re-exports individual components and nested barrels:
export * from './popup';
export * from './icons';
export {Age} from './Age';
export {Alert} from './Alert';
export {Card} from './Card';
export {DonutChart} from './DonutChart';
export {Dropdown} from './Dropdown';
export {FilterBar} from './FilterBar';
export {Form} from './Form';
export {Icon} from './Icon';
export {Link} from './Link';
export {Modal} from './Modal';
export {PopupMenu} from './PopupMenu';
export {ResourceListV2} from './ResourceListV2';
export {Spinner} from './Spinner';
export {Table} from './Table';
export {Tooltip} from './Tooltip';Notice how this barrel also re-exports from nested barrels (./popup, ./icons), creating a hierarchical structure.
The icons barrel groups over 20 icon components, making them easy to import together:
export {ClusterRoleIcon} from './ClusterRoleIcon';
export {ConfigMapIcon} from './ConfigMapIcon';
export {CronJobIcon} from './CronJobIcon';
export {DaemonSetIcon} from './DaemonSetIcon';
export {DeploymentIcon} from './DeploymentIcon';
export {IngressIcon} from './IngressIcon';
export {JobIcon} from './JobIcon';
export {KubernetesIcon} from './KubernetesIcon';
export {NamespaceIcon} from './NamespaceIcon';
export {NodeIcon} from './NodeIcon';
export {PodIcon} from './PodIcon';
export {SecretIcon} from './SecretIcon';
export {ServiceIcon} from './ServiceIcon';
// ... and moreElectronIM
ElectronIM is an Electron-based instant messaging client. Its barrel files organize both components and shared utilities, demonstrating how barrels can consolidate different types of exports.
The components barrel re-exports Preact hooks alongside UI components:
export {APP_EVENTS, CLOSE_BUTTON_BEHAVIORS, ELECTRONIM_VERSION} from '../../bundles/constants.mjs';
export {createRef, html, render, useLayoutEffect, useReducer, useState} from '../../bundles/preact.mjs';
export {Button} from './button.mjs';
export {Card} from './card.mjs';
export {Checkbox} from './checkbox.mjs';
export {Icon} from './icon.mjs';
export {IconButton} from './icon-button.mjs';
export {Logo} from './electronim.mjs';
export {Menu} from './menu.mjs';
export {NavigationRail} from './navigation-rail.mjs';
export {Select} from './select.mjs';
export {Switch} from './switch.mjs';
export {TextField} from './text-field.mjs';
export {TopAppBar} from './top-app-bar.mjs';This approach provides a single import point for everything a component might need: UI primitives, hooks, and application constants.
Pros of Barrel Exports
Simplified imports
Barrels reduce import clutter by consolidating multiple imports into one. This becomes especially helpful in UI-heavy codebases where components frequently import from the same directories.
Encapsulation
Barrels hide your internal directory structure from consumers. You can reorganize, rename, or split files without affecting external imports.
Clear public API
By explicitly listing exports, you define what's public and what's internal. Components not exported from the barrel remain implicitly private to the module.
Easier refactoring
When you move a file, you only need to update the barrel, not every file that imports it.
Named exports preserve component names
Unlike export default, barrel exports encourage named exports, which maintain consistent naming across your codebase:
// With default exports - names can vary unpredictably
import Button from './components/Button';
import Btn from './components/Button'; // Same component, different name!
import MyButton from './components/Button'; // Confusing
// With barrel exports using named exports - consistent naming
import {Button} from './components';
// "Button" is always "Button" everywhereThis consistency makes your code more searchable and self-documenting.
Cons of Barrel Exports
Performance impact
When you import from a barrel, bundlers and tools may process the entire module, including dependencies you don't use. This can slow down builds and tests significantly.
Warning
Atlassian reported a 75% reduction in build times after removing barrel files from their Jira frontend.
Circular dependency risk
Barrels can create circular imports.
If tab-panel.js imports from index.js, and index.js re-exports from tab-panel.js, you have a cycle.
Caution
JavaScript handles circular dependencies somewhat gracefully, but bundlers may crash with obscure errors.
IDE navigation
"Go to definition" might take you to the barrel file instead of the actual implementation, adding an extra step when navigating code.
Tree-shaking challenges
Older bundlers and configurations struggle to eliminate unused exports from barrels, potentially increasing bundle size. This affects Webpack 4, older Rollup configurations without proper tree-shaking plugins, and Jest transformations that don't respect ES module semantics.
When to Use Barrel Exports
Use them for:
- Library entry points: Libraries need a single entry point for
package.json's "main" field - Component libraries: When you have a stable set of components frequently imported together
- Icon libraries: Perfect for grouping many similar small modules
- Grouping related functionality: Redux actions, API clients, utility functions
Avoid them in:
- Large application codebases: Where build performance is critical
- Directories with many modules: When only a few modules get imported at once
- Performance-sensitive applications: Where every millisecond of build time matters
Best Practices
1. Prefer named exports over wildcards
Tip
Use export {Foo} from './Foo' instead of export * from './Foo'.
Wildcard re-exports (export *) have several downsides:
- Accidental exports: Internal helpers or debug utilities can leak into your public API
- Naming collisions: Two files exporting the same name cause silent overwrites
- Circular dependency risk: Wildcards make it harder to trace what gets imported where
- Worse IDE support: Autocomplete can't show you what's available without evaluating the entire module graph
Named exports make your barrel's public API explicit and intentional.
2. Keep barrels focused
Don't create mega-barrels that export hundreds of items. Split large directories into sub-barrels with clear boundaries.
3. Use ESLint plugins
Tools like eslint-plugin-barrel-files can enforce consistent patterns and catch potential issues early.
4. Consider granular entry points for libraries
Instead of one massive barrel, provide multiple smaller entry points (e.g., my-lib/components, my-lib/utils).
This gives consumers more control over what they import.
5. Watch for circular dependencies
Be mindful when modules in the same directory import from each other. If file A imports from the barrel and the barrel re-exports A, you have a cycle.
Barrels and TypeScript
TypeScript works well with barrel exports, but there are a few quirks to keep in mind.
Incremental compilation in watch mode
Barrel files can sometimes confuse TypeScript's incremental compiler during tsc --watch or when using ts-node.
When you modify a file that's re-exported through a barrel, TypeScript may not always detect the change correctly.
If you notice stale types during development, a full rebuild usually resolves the issue.
Path aliases and barrels
TypeScript's paths option in tsconfig.json pairs nicely with barrels.
You can create clean import paths that point to your barrel files:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@components/*": ["src/components/*"],
"@components": ["src/components/index.ts"]
}
}
}This lets you write import {Button} from '@components' instead of relative paths.
Avoid duplicate type names
Don't barrel-export types with the same name from multiple files. Unlike runtime values that overwrite silently, duplicate type exports cause TypeScript compilation errors:
// src/api/types.ts
export interface Response { /* ... */ }
// src/http/types.ts
export interface Response { /* ... */ }
// src/index.ts
export * from './api/types';
export * from './http/types'; // Error: Duplicate identifier 'Response'Use named exports or rename conflicting types before re-exporting.
Conclusion
Barrel exports provide a useful pattern for organizing JavaScript and TypeScript projects. They simplify imports, encourage consistent naming, and create clear module boundaries.
However, they come with trade-offs. For large applications where build performance matters, you might want to avoid them or use them sparingly.
For libraries and component collections, barrels remain an excellent choice for providing a clean, user-friendly API.
