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.

7 min read

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:

typescripttypescript
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:

typescripttypescript
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:

typescripttypescript
// 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:

typescripttypescript
// 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:

typescripttypescript
// 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.

typescripttypescript
// 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:

typescripttypescript
// 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 barrelKeep internal
Public functions and classesPrivate helper functions
Public interfaces and type aliasesInternal-only types
Constants that consumers needConfiguration constants used internally
Error classes thrown to consumersImplementation 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.

typescripttypescript
// 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.

Circular dependency through barrel files

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

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.
RunePowered by Rune AI

Frequently Asked Questions

Do barrel files affect runtime performance?

Barrel files themselves do not affect runtime performance because they are just re-export statements. However, importing from a barrel that re-exports many modules can trigger loading all those modules even if you only need one export, which can slow down startup in large codebases.

Should every folder have an index.ts barrel?

No. Only create barrel files for folders whose exports are consumed as a group by external code. For internal implementation folders, barrel files add maintenance overhead without benefit.

Can barrel files cause circular dependencies?

Yes. If barrel A re-exports from modules that import from barrel B, and barrel B re-exports from modules that import from barrel A, you have an undetected circular dependency. Keep barrel files at module boundaries, not deep inside them.

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.