Skip to lesson

learningtypescript.org / intermediate / 08-modules · lesson 8 of 25

TL;DR

Organize TypeScript code with ES modules. Learn named exports, default exports, type-only imports, re-exports, and barrel files.

Key concepts

  • TypeScript modules
  • TypeScript import export
  • type-only imports
  • barrel files TypeScript

Modules

Modules let you split your code into separate files, each with its own scope. TypeScript uses ES modules with import and export statements, adding type-only imports for better performance.

Named Exports

Named exports let you export multiple values from a module. Importers must use the exact names.

// Simulating module exports in a single file

// Named exports
const PI = 3.14159;
const E = 2.71828;

function add(a: number, b: number): number {
  return a + b;
}

function multiply(a: number, b: number): number {
  return a * b;
}

interface MathResult {
  operation: string;
  result: number;
}

function calculate(op: string, a: number, b: number): MathResult {
  const result = op === "add" ? add(a, b) : multiply(a, b);
  return { operation: op, result };
}

// Using the exported values
console.log(`PI = ${PI}`);
console.log(`E = ${E}`);
console.log(`add(3, 4) = ${add(3, 4)}`);
console.log(`multiply(5, 6) = ${multiply(5, 6)}`);

const calc = calculate("add", 10, 20);
console.log(`${calc.operation}(10, 20) = ${calc.result}`);

Default Exports

A module can have one default export, typically its primary value.

// Default export pattern — one main export per module
class Logger {
  private prefix: string;
  private logs: string[] = [];

  constructor(prefix: string) {
    this.prefix = prefix;
  }

  log(message: string): void {
    const entry = `[${this.prefix}] ${message}`;
    this.logs.push(entry);
    console.log(entry);
  }

  warn(message: string): void {
    const entry = `[${this.prefix}] WARNING: ${message}`;
    this.logs.push(entry);
    console.log(entry);
  }

  getHistory(): string[] {
    return [...this.logs];
  }
}

// Using the default export
const logger = new Logger("App");
logger.log("Application started");
logger.log("Loading configuration");
logger.warn("Config file not found, using defaults");

console.log(`\nLog history: ${logger.getHistory().length} entries`);

Namespace Imports and Re-exports

Group all exports under a single name, or re-export from other modules.

// Namespace import pattern: import * as Name
// Simulating with an object

const MathUtils = {
  PI: 3.14159,
  E: 2.71828,
  add: (a: number, b: number) => a + b,
  subtract: (a: number, b: number) => a - b,
  multiply: (a: number, b: number) => a * b,
  divide: (a: number, b: number) => b !== 0 ? a / b : NaN,
};

const StringUtils = {
  capitalize: (s: string) => s.charAt(0).toUpperCase() + s.slice(1),
  reverse: (s: string) => s.split("").reverse().join(""),
  truncate: (s: string, len: number) =>
    s.length > len ? s.slice(0, len) + "..." : s,
};

// Barrel file pattern — re-export from index
// In real code: export { MathUtils } from './math';
//               export { StringUtils } from './strings';

console.log("Math operations:");
console.log(`  PI = ${MathUtils.PI}`);
console.log(`  3 + 4 = ${MathUtils.add(3, 4)}`);
console.log(`  10 / 3 = ${MathUtils.divide(10, 3).toFixed(4)}`);

console.log("\nString operations:");
console.log(`  capitalize: ${StringUtils.capitalize("hello world")}`);
console.log(`  reverse: ${StringUtils.reverse("TypeScript")}`);
console.log(`  truncate: ${StringUtils.truncate("A very long string", 10)}`);

Type-Only Imports

TypeScript supports importing only types, which are erased at compile time.

// Type-only imports are erased at runtime
// In real code: import type { User } from './types';

// Types and interfaces
type UserRole = "admin" | "editor" | "viewer";

interface User {
  id: number;
  name: string;
  role: UserRole;
}

interface Post {
  id: number;
  title: string;
  authorId: number;
}

// Runtime values that use the types
function createUser(name: string, role: UserRole): User {
  return { id: Math.floor(Math.random() * 1000), name, role };
}

function getUserPosts(user: User): Post[] {
  return [
    { id: 1, title: `${user.name}'s First Post`, authorId: user.id },
    { id: 2, title: `${user.name}'s Second Post`, authorId: user.id },
  ];
}

function formatUser(user: User): string {
  return `${user.name} (${user.role}) #${user.id}`;
}

const admin = createUser("Alice", "admin");
const posts = getUserPosts(admin);

console.log(`User: ${formatUser(admin)}`);
console.log(`Posts:`);
posts.forEach(p => console.log(`  - ${p.title}`));

Try It Yourself

// Build a modular task management system

// Types module
type Priority = "low" | "medium" | "high";
type TaskStatus = "todo" | "in-progress" | "done";

interface Task {
  id: number;
  title: string;
  priority: Priority;
  status: TaskStatus;
}

// Service module
class TaskService {
  private tasks: Task[] = [];
  private nextId = 1;

  addTask(title: string, priority: Priority): Task {
    const task: Task = {
      id: this.nextId++,
      title,
      priority,
      status: "todo"
    };
    this.tasks.push(task);
    return task;
  }

  updateStatus(id: number, status: TaskStatus): void {
    const task = this.tasks.find(t => t.id === id);
    if (task) task.status = status;
  }

  getByPriority(priority: Priority): Task[] {
    return this.tasks.filter(t => t.priority === priority);
  }

  getByStatus(status: TaskStatus): Task[] {
    return this.tasks.filter(t => t.status === status);
  }

  getSummary(): Record<TaskStatus, number> {
    return {
      "todo": this.tasks.filter(t => t.status === "todo").length,
      "in-progress": this.tasks.filter(t => t.status === "in-progress").length,
      "done": this.tasks.filter(t => t.status === "done").length,
    };
  }
}

// App module — uses both
const service = new TaskService();

service.addTask("Write docs", "high");
service.addTask("Fix bug", "high");
service.addTask("Add tests", "medium");
service.addTask("Update README", "low");

service.updateStatus(1, "done");
service.updateStatus(2, "in-progress");

console.log("Task Summary:", JSON.stringify(service.getSummary()));

console.log("\nHigh priority:");
service.getByPriority("high").forEach(t =>
  console.log(`  [${t.status}] ${t.title}`)
);

console.log("\nIn progress:");
service.getByStatus("in-progress").forEach(t =>
  console.log(`  ${t.title} (${t.priority})`)
);

Key Takeaways

  • Each TypeScript file is a module with its own scope
  • Use named exports when a module provides multiple items
  • Use default exports for a module's primary value
  • Namespace imports (import * as X) group all exports under one name
  • Barrel files (index.ts) simplify imports by re-exporting from a single entry point
  • Type-only imports (import type) help with tree-shaking
  • Avoid circular dependencies by putting shared types in separate modules

Pro Tip: Follow "one thing per file" for large types and classes, but group small related utilities together. Use barrel files for clean public APIs, but only re-export what consumers actually need.

Next Steps

Now that you can organize code across modules, you need to know what happens when things go wrong. Next, you'll learn how TypeScript handles errors — try/catch with type narrowing, custom error classes, and the Result pattern.

Two-tier handoff: this document is the complete reading surface. Continue learning for stateful practice, progress, and real sandbox execution.