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
- Barrels are index files: They re-export from multiple modules
- Use
export * from: Re-export all named exports - Use
export { x } from: Selective re-exports - Rename with
as: Avoid conflicts and improve naming - Avoid circular imports: Import directly within the same folder
- Keep barrels simple: Only re-exports, no logic
- 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.