Barrel Files in TypeScript
Barrel files consolidate multiple module exports into a single import path. Learn how to create them, when they help, and the performance and circular-dependency traps to avoid.
A TypeScript barrel file is a module that re-exports from several other modules, giving consumers a single import path instead of many. It is almost always named index.ts and placed in a directory to represent that directory's public API. The barrel hides the internal file structure from consumers, so you can rename or reorganize files without changing any import statement outside the directory.
TypeScript barrel files solve a common organization problem. Without one, importing from three sibling modules requires three separate import statements, each pointing to a different file:
import { createUser } from "./users/create.js";
import { updateUser } from "./users/update.js";
import { deleteUser } from "./users/delete.js";With a barrel file, all three imports collapse into a single line pointing to one path, making the consumer code shorter and insulating it from internal file renames:
import { createUser, updateUser, deleteUser } from "./users/index.js";The barrel file hides the internal file structure from consumers. If you later rename create.ts to user-factory.ts, only the barrel file needs to change. Every consumer keeps the same import path and the same exported names.
Creating a Barrel File
A barrel file is just a module full of re-export statements. Start with three implementation modules that each export a focused set of functionality. The first handles user creation:
// users/create.ts
export interface CreateUserInput {
name: string;
email: string;
}
export function createUser(input: CreateUserInput) {
return { id: crypto.randomUUID(), ...input };
}The second handles updates, accepting partial data through optional properties so callers only need to provide the fields that changed:
// users/update.ts
export interface UpdateUserInput {
name?: string;
email?: string;
}
export function updateUser(id: string, input: UpdateUserInput) {
return { id, ...input };
}The third handles deletion. It takes no input object, only an ID, and logs the removed user's ID to confirm which record was deleted:
// users/delete.ts
export function deleteUser(id: string): void {
console.log(`Deleted user ${id}`);
}Now create the barrel file that exposes the public API. It re-exports the three functions as named exports and the two interfaces as type-only exports.
// users/index.ts
export { createUser } from "./create.js";
export { updateUser } from "./update.js";
export { deleteUser } from "./delete.js";
export type { CreateUserInput, UpdateUserInput } from "./create.js";The barrel does not add any logic of its own. It only forwards names from the three implementation files, so the runtime cost of adding it is effectively zero.
A consumer now imports both the function and its input type from that single barrel path instead of reaching into the individual files:
// app.ts
import { createUser } from "./users/index.js";
import type { CreateUserInput } from "./users/index.js";
const input: CreateUserInput = { name: "Ada", email: "ada@example.com" };
const user = createUser(input);
console.log(user);The consumer does not know or care that createUser lives in users/create.ts. The barrel abstracts the internal file layout. Running this code prints an object with an id, name, and email to the console.
What Goes in a Barrel
A barrel file should export the module group's public API. Decide what belongs in the barrel by asking: would a consumer of this directory reasonably need this export?
| Include in barrel | Keep internal |
|---|---|
| Public functions and classes | Private helper functions |
| Public interfaces and type aliases | Internal-only types |
| Constants that consumers need | Configuration constants used internally |
| Error classes thrown to consumers | Implementation details |
A good barrel is small and focused. If your barrel re-exports thirty things from fifteen files, the module group is probably too broad.
Split it into smaller groups, each with its own barrel. The barrel should feel like a curated list, not a dump of every symbol in the directory.
Barrel File Patterns
Single-directory barrel is the simplest pattern: one index.ts that re-exports everything in its directory. This is common in component libraries where consumers import from a single package path.
Nested barrels use a top-level barrel to re-export from sub-directory barrels. You can use the wildcard export syntax with export * from, but be careful: it re-exports every named export from the target module, including internal exports you may not want to expose.
A safer alternative is explicit re-exports where you list every exported name directly.
Wildcard re-exports with export * from are concise but risky. If two modules both export something called format, the barrel has a naming collision, and TypeScript only reports the error when a consumer tries to import the conflicting name.
Explicit re-exports avoid this problem entirely by making every exported name visible in the barrel file.
Type-Only Re-Exports in Barrels
Barrel files should use export type for type-only re-exports. This keeps the barrel's runtime footprint minimal and prevents accidental value emission.
// types/index.ts
export type { User, CreateUserInput, UpdateUserInput } from "../users/create.js";
export type { Post, PostStatus } from "../posts/types.js";Without export type, TypeScript may still elide type-only re-exports automatically, but being explicit ensures the behavior is predictable across all module settings. For more on the syntax, see type-only imports in TypeScript.
Barrel Files and Circular Dependencies
Barrel files can hide circular dependencies. Consider this structure shown in the diagram below.
The barrel files create an indirect cycle. The users barrel re-exports from users/create.ts, which imports from the posts barrel, which re-exports from posts/publish.ts, which imports from the users barrel. The cycle is not visible by looking at any single file.
To avoid this, keep barrels at the boundaries of your module groups and never import from a barrel inside the same group. Implementation files should import directly from sibling implementation files, not through the barrel. For more strategies, see avoid circular imports in TypeScript.
When Not to Use Barrel Files
Barrel files are not always the right choice. Deep barrel chains slow editor startup because TypeScript must resolve each barrel and all its re-exports to provide accurate autocomplete. In very large projects, this chain can add noticeable latency.
Barrels can also cause over-importing during development. If a barrel re-exports twenty functions and you only need one, TypeScript's language server still loads all the re-exported modules to provide accurate autocomplete, even though modern bundlers can tree-shake the unused exports from the final build.
Internal code does not need barrels at all. Barrels are meant for consumers outside the module group. Inside a module group, direct imports between sibling files are simpler, faster to resolve, and easier to trace.
Organizing a Project with Barrels
A practical project layout uses barrels at module boundaries but not inside implementation folders. Each directory that represents a cohesive module gets its own index.ts barrel. The top-level barrel re-exports from each module barrel, giving the entire package a single entry point.
Implementation files inside a module import directly from each other using relative paths. They never go through their own barrel.
Consumers outside the module import from the barrel instead. This creates a clean boundary: the barrel is the public API contract, and everything behind it can change freely.
Rune AI
Key Insights
- A barrel file is a module that re-exports from other modules, typically named index.ts.
- Barrels reduce import statement count and give consumers a single entry point.
- Use export type for type-only re-exports to keep the barrel's runtime footprint minimal.
- Avoid barrel files that re-export from other barrel files to prevent circular dependencies.
- In large projects, deep barrel chains can slow editor startup. Prefer direct imports for performance-critical paths.
Frequently Asked Questions
Do barrel files affect runtime performance?
Should every folder have an index.ts barrel?
Can barrel files cause circular dependencies?
Conclusion
Barrel files are a simple but powerful pattern for organizing module exports. A single index.ts can replace many scattered import paths and give consumers a clean public API. Use them at module boundaries, keep type re-exports explicit with export type, group related modules into coherent barrels, and avoid nesting barrels that create circular dependencies.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.