From Zero to AI

Lesson 6.3: Barrel Exports

Duration: 45 minutes

Learning Objectives

By the end of this lesson, you will be able to:

  • Understand what barrel exports are and why they are useful
  • Create index files that aggregate exports from multiple modules
  • Use selective re-exports to control your public API
  • Avoid common pitfalls with barrel exports
  • Structure barrels for optimal performance

What Are Barrel Exports?

A barrel is an index.ts file that re-exports items from other files in the same folder. Think of it as a "front door" to a folder:

models/
├── user.ts          # Exports User, CreateUserInput
├── product.ts       # Exports Product, ProductCategory
├── order.ts         # Exports Order, OrderStatus
└── index.ts         # The barrel - re-exports everything

Without a barrel:

// Tedious: import from each file
import { Order, OrderStatus } from './models/order';
import { Product, ProductCategory } from './models/product';
import { CreateUserInput, User } from './models/user';

With a barrel:

// Clean: import from the folder
import { Order, Product, User } from './models';

Creating a Basic Barrel

The simplest barrel re-exports everything from each file:

// models/user.ts
export interface User {
  id: number;
  name: string;
  email: string;
}

export interface CreateUserInput {
  name: string;
  email: string;
}

// models/product.ts
export interface Product {
  id: number;
  name: string;
  price: number;
}

export type ProductCategory = 'electronics' | 'clothing' | 'food';

// models/order.ts
export interface Order {
  id: number;
  userId: number;
  products: Product[];
  total: number;
}

export type OrderStatus = 'pending' | 'shipped' | 'delivered';

Create the barrel:

// models/index.ts
export * from './user';
export * from './product';
export * from './order';

Now all exports are available from one import:

// main.ts
import { CreateUserInput, Order, OrderStatus, Product, ProductCategory, User } from './models';

Selective Re-exports

Sometimes you want to control what is publicly available. Use selective exports:

// utils/internal-helpers.ts
export function internalHelper(): void {
  // This should not be public
}

export function publicHelper(): void {
  // This should be public
}

// utils/string-utils.ts
export function capitalize(str: string): string {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

export function slugify(str: string): string {
  return str.toLowerCase().replace(/\s+/g, '-');
}

// utils/index.ts
// Only export what should be public
export { publicHelper } from './internal-helpers';
export { capitalize, slugify } from './string-utils';
// internalHelper is NOT exported

Now internalHelper is hidden from external consumers:

// main.ts
import { capitalize, publicHelper, slugify } from './utils';

// internalHelper is not accessible

Re-exporting with Renames

You can rename exports in the barrel to avoid conflicts or provide better names:

// database/user-repository.ts
export function find(id: number): User | undefined {
  // ...
}

export function create(user: CreateUserInput): User {
  // ...
}

// database/product-repository.ts
export function find(id: number): Product | undefined {
  // ...
}

export function create(product: CreateProductInput): Product {
  // ...
}

Both files have find and create. Rename in the barrel:

// database/index.ts
export { find as findUser, create as createUser } from './user-repository';

export { find as findProduct, create as createProduct } from './product-repository';

Now names are unique:

// main.ts
import { createProduct, createUser, findProduct, findUser } from './database';

Re-exporting Default Exports

Default exports need special handling in barrels:

// services/user-service.ts
export default class UserService {
  // ...
}

// services/product-service.ts
export default class ProductService {
  // ...
}

Re-export with names:

// services/index.ts
export { default as UserService } from './user-service';
export { default as ProductService } from './product-service';

Now import as named exports:

// main.ts
import { ProductService, UserService } from './services';

Combining Named and Default Re-exports

When a file has both default and named exports:

// logger/logger.ts
export default class Logger {
  log(message: string): void {
    console.log(message);
  }
}

export enum LogLevel {
  Debug,
  Info,
  Warning,
  Error,
}

export interface LogEntry {
  level: LogLevel;
  message: string;
  timestamp: Date;
}

Re-export everything:

// logger/index.ts
export { default as Logger, LogLevel, LogEntry } from './logger';

// Or in two lines:
export { default as Logger } from './logger';
export { LogLevel, LogEntry } from './logger';

// Or re-export all named and default separately:
export * from './logger'; // Named exports
export { default as Logger } from './logger'; // Default export

Nested Barrels

Large projects can have barrels at multiple levels:

src/
├── features/
│   ├── users/
│   │   ├── user.model.ts
│   │   ├── user.service.ts
│   │   └── index.ts          # Users barrel
│   ├── products/
│   │   ├── product.model.ts
│   │   ├── product.service.ts
│   │   └── index.ts          # Products barrel
│   └── index.ts              # Features barrel
└── index.ts                  # Root barrel

Each level aggregates the level below:

// features/users/index.ts
export * from './user.model';
export * from './user.service';

// features/products/index.ts
export * from './product.model';
export * from './product.service';

// features/index.ts
export * from './users';
export * from './products';

// src/index.ts
export * from './features';

Import from any level:

// Deep import
import { User } from "./features/users";

// Or from features
import { User, Product } from "./features";

// Or from root
import { User, Product } from "./src";

Type-Only Re-exports

When re-exporting only types, use export type:

// types/user-types.ts
export interface User {
  id: number;
  name: string;
}

export type UserRole = 'admin' | 'user';

// types/product-types.ts
export interface Product {
  id: number;
  name: string;
  price: number;
}

// types/index.ts
export type { User, UserRole } from './user-types';
export type { Product } from './product-types';

This tells TypeScript these are type-only exports, which can improve build performance.


Benefits of Barrel Exports

1. Cleaner Imports

// Without barrels
import { User } from "./models/user";
import { Product } from "./models/product";
import { Order } from "./models/order";

// With barrels
import { User, Product, Order } from "./models";

2. Encapsulation

Control what is public and what is internal:

// utils/index.ts
// Only expose public API
export { format } from './format';
export { validate } from './validate';
// privateHelper stays hidden

3. Easier Refactoring

Move files around without changing imports:

// Before: user.ts is in models/
import { User } from "./models";

// After: moved to models/entities/user.ts
// Barrel handles it - import stays the same!
import { User } from "./models";

4. Consistent API

One place defines what a module exposes:

// models/index.ts - the public API
export { User, CreateUserInput } from './user';
export { Product } from './product';
// Other internal types stay hidden

Common Pitfalls

Pitfall 1: Circular Dependencies

Barrels can create circular dependencies:

// models/user.ts
import { Order } from './index';
// models/order.ts
import { User } from './index';

// Imports from barrel
export interface User {
  orders: Order[];
}

// Also imports from barrel
export interface Order {
  user: User;
}

// models/index.ts
export * from './user'; // user imports from index
export * from './order'; // order imports from index
// CIRCULAR DEPENDENCY!

Solution: Import directly, not through the barrel:

// models/user.ts
import { Order } from './order';
// models/order.ts
import { User } from './user';

// Direct import
export interface User {
  orders: Order[];
}

// Direct import
export interface Order {
  user: User;
}

Pitfall 2: Export Name Conflicts

Two files export the same name:

// utils/string.ts
export function format(str: string): string {}

// utils/date.ts
export function format(date: Date): string {}

// utils/index.ts
export * from './string'; // exports format
export * from './date'; // also exports format - ERROR!

Solution: Rename in the barrel:

// utils/index.ts
export { format as formatString } from './string';
export { format as formatDate } from './date';

Pitfall 3: Performance with Large Barrels

Importing from a barrel loads all re-exported modules:

// 150 KB
// main.ts
import { tinyFunction } from './huge-barrel';

// huge-barrel/index.ts
export * from './module1'; // 100 KB
export * from './module2'; // 200 KB
export * from './module3';

// Loads all 450 KB even though you need one function!

Solution: Use direct imports for performance-critical code, or split barrels:

// Direct import - only loads what you need
import { tinyFunction } from './huge-barrel/module1';

Pitfall 4: IDE Auto-import Issues

IDEs might auto-import from the file instead of the barrel:

// IDE might generate this:
import { User } from "./models/user";

// Instead of preferred:
import { User } from "./models";

Solution: Configure your IDE or use import sorting tools.


Best Practices

1. One Barrel Per Folder

Each folder with multiple files should have one index.ts:

models/
├── user.ts
├── product.ts
└── index.ts  ✓

2. Keep Barrels Simple

Only re-exports, no logic:

// Good: only re-exports
export * from './user';
export * from './product';

// Bad: logic in barrel
export * from './user';
const defaultUser = { name: 'Guest' }; // Don't do this
export { defaultUser };

3. Be Explicit About Public API

Use selective exports for libraries:

// Good: explicit public API
export { User, CreateUserInput } from './user';
export { Product } from './product';

// Avoid: exporting everything blindly
export * from './user'; // Might expose internals

4. Document Your Barrels

Add comments for maintainability:

// models/index.ts
/**
 * Public models for the application.
 * @module models
 */

// User-related exports
export { User, CreateUserInput, UpdateUserInput } from './user';

// Product-related exports
export { Product, ProductCategory } from './product';

// Order-related exports
export { Order, OrderStatus } from './order';

Exercises

Exercise 1: Create a Services Barrel

Given these service files, create a barrel:

// services/auth-service.ts
export class AuthService {
  login(email: string, password: string): boolean {
    return true;
  }
  logout(): void {}
}

// services/email-service.ts
export class EmailService {
  send(to: string, subject: string, body: string): void {}
}

// services/storage-service.ts
export class StorageService {
  save(key: string, value: string): void {}
  load(key: string): string | null {
    return null;
  }
}

Create services/index.ts and use it:

// main.ts
import { AuthService, EmailService, StorageService } from './services';
Solution
// main.ts
import { AuthService, EmailService, StorageService } from './services';

// services/index.ts
export { AuthService } from './auth-service';
export { EmailService } from './email-service';
export { StorageService } from './storage-service';

const auth = new AuthService();
const email = new EmailService();
const storage = new StorageService();

auth.login('user@example.com', 'password');
email.send('admin@example.com', 'Login Alert', 'User logged in');
storage.save('lastLogin', new Date().toISOString());

Exercise 2: Selective Exports

Create a barrel that hides internal utilities:

// utils/public.ts
export function formatDate(date: Date): string {
  return date.toISOString().split('T')[0];
}

export function formatCurrency(amount: number): string {
  return `$${amount.toFixed(2)}`;
}

// utils/internal.ts
export function debugLog(message: string): void {
  console.log(`[DEBUG] ${message}`);
}

export function measureTime<T>(fn: () => T): T {
  const start = Date.now();
  const result = fn();
  debugLog(`Execution time: ${Date.now() - start}ms`);
  return result;
}

Create a barrel that exports only formatDate and formatCurrency.

Solution
// debugLog and measureTime from internal.ts are NOT exported
// They can still be used internally by importing directly:
// import { debugLog } from "./internal";
// main.ts
import { formatCurrency, formatDate } from './utils';

// utils/index.ts
// Public API - only expose formatting utilities
export { formatDate, formatCurrency } from './public';

console.log(formatDate(new Date())); // "2024-01-15"
console.log(formatCurrency(99.99)); // "$99.99"

// This would error - not exported:
// import { debugLog } from "./utils";

Exercise 3: Handle Name Conflicts

Two modules export create. Create a barrel with renamed exports:

// factories/user-factory.ts
export function create(name: string): { type: 'user'; name: string } {
  return { type: 'user', name };
}

// factories/product-factory.ts
export function create(
  name: string,
  price: number
): { type: 'product'; name: string; price: number } {
  return { type: 'product', name, price };
}
Solution
// main.ts
import { createProduct, createUser } from './factories';

// factories/index.ts
export { create as createUser } from './user-factory';
export { create as createProduct } from './product-factory';

const user = createUser('Alice');
// { type: "user", name: "Alice" }

const product = createProduct('Laptop', 999);
// { type: "product", name: "Laptop", price: 999 }

Exercise 4: Nested Barrels

Create a nested barrel structure:

features/
├── auth/
│   ├── auth.service.ts
│   ├── auth.types.ts
│   └── index.ts
├── users/
│   ├── user.service.ts
│   ├── user.types.ts
│   └── index.ts
└── index.ts
Solution
// features/auth/auth.types.ts
export interface Credentials {
  email: string;
  password: string;
}

export interface AuthToken {
  token: string;
  expiresAt: Date;
}

// features/auth/auth.service.ts
import type { Credentials, AuthToken } from "./auth.types";

export class AuthService {
  login(credentials: Credentials): AuthToken {
    return {
      token: "fake-token",
      expiresAt: new Date(Date.now() + 3600000)
    };
  }
}

// features/auth/index.ts
export * from "./auth.types";
export { AuthService } from "./auth.service";

// features/users/user.types.ts
export interface User {
  id: number;
  name: string;
  email: string;
}

// features/users/user.service.ts
import type { User } from "./user.types";

export class UserService {
  findById(id: number): User | undefined {
    return { id, name: "Test User", email: "test@example.com" };
  }
}

// features/users/index.ts
export * from "./user.types";
export { UserService } from "./user.service";

// features/index.ts
export * from "./auth";
export * from "./users";

// main.ts - can import from any level
import { AuthService, UserService, User, Credentials } from "./features";
// Or more specific:
import { AuthService } from "./features/auth";
import { User } from "./features/users";

Key Takeaways

  1. Barrels are index files: They re-export from multiple modules
  2. Use export * from: Re-export all named exports
  3. Use export { x } from: Selective re-exports
  4. Rename with as: Avoid conflicts and improve naming
  5. Avoid circular imports: Import directly within the same folder
  6. Keep barrels simple: Only re-exports, no logic
  7. Control your public API: Be explicit about what you export

Resources

Resource Type Description
TypeScript Handbook: Modules Documentation Official module guide
Barrel Pattern Tutorial Detailed barrel explanation
Module Best Practices Documentation Library structuring

Next Lesson

Now let us put everything together by refactoring a real project.

Continue to Lesson 6.4: Practice - Project Refactoring