From Zero to AI

Lesson 6.4: Practice - Project Refactoring

Duration: 90 minutes

Learning Objectives

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

  • Refactor a single-file application into a modular structure
  • Apply module organization patterns to real code
  • Create a clean project architecture with proper separation of concerns
  • Use barrel exports to create a professional public API

The Challenge

We will take a TodoList application written in a single file and refactor it into a well-organized, modular project. This is a common real-world task when codebases grow.


Starting Point: Single-File Application

Here is our starting code - everything in one file:

// todo-app.ts - Everything in one file (200+ lines)

// ============ Types ============
interface Todo {
  id: number;
  title: string;
  description: string;
  completed: boolean;
  priority: Priority;
  dueDate: Date | null;
  tags: string[];
  createdAt: Date;
  updatedAt: Date;
}

type Priority = 'low' | 'medium' | 'high';

interface CreateTodoInput {
  title: string;
  description?: string;
  priority?: Priority;
  dueDate?: Date;
  tags?: string[];
}

interface UpdateTodoInput {
  title?: string;
  description?: string;
  priority?: Priority;
  dueDate?: Date | null;
  tags?: string[];
}

interface TodoFilter {
  completed?: boolean;
  priority?: Priority;
  tag?: string;
  dueBefore?: Date;
  dueAfter?: Date;
}

interface TodoStats {
  total: number;
  completed: number;
  pending: number;
  overdue: number;
  byPriority: Record<Priority, number>;
}

// ============ Validation ============
function validateTitle(title: string): void {
  if (!title || title.trim().length === 0) {
    throw new Error('Title is required');
  }
  if (title.length > 100) {
    throw new Error('Title must be 100 characters or less');
  }
}

function validateDescription(description: string): void {
  if (description.length > 500) {
    throw new Error('Description must be 500 characters or less');
  }
}

function validateTags(tags: string[]): void {
  if (tags.length > 10) {
    throw new Error('Maximum 10 tags allowed');
  }
  for (const tag of tags) {
    if (tag.length > 20) {
      throw new Error('Tag must be 20 characters or less');
    }
  }
}

// ============ Formatting ============
function formatDate(date: Date): string {
  return date.toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'short',
    day: 'numeric',
  });
}

function formatPriority(priority: Priority): string {
  const icons: Record<Priority, string> = {
    low: '[ ]',
    medium: '[*]',
    high: '[!]',
  };
  return icons[priority];
}

function formatTodo(todo: Todo): string {
  const status = todo.completed ? '[x]' : '[ ]';
  const priority = formatPriority(todo.priority);
  const due = todo.dueDate ? ` (Due: ${formatDate(todo.dueDate)})` : '';
  const tags = todo.tags.length > 0 ? ` [${todo.tags.join(', ')}]` : '';
  return `${status} ${priority} ${todo.title}${due}${tags}`;
}

// ============ Date Utilities ============
function isOverdue(date: Date): boolean {
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  return date < today;
}

function daysUntil(date: Date): number {
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const target = new Date(date);
  target.setHours(0, 0, 0, 0);
  const diff = target.getTime() - today.getTime();
  return Math.ceil(diff / (1000 * 60 * 60 * 24));
}

function addDays(date: Date, days: number): Date {
  const result = new Date(date);
  result.setDate(result.getDate() + days);
  return result;
}

// ============ TodoService ============
class TodoService {
  private todos: Todo[] = [];
  private nextId = 1;

  create(input: CreateTodoInput): Todo {
    validateTitle(input.title);
    if (input.description) {
      validateDescription(input.description);
    }
    if (input.tags) {
      validateTags(input.tags);
    }

    const now = new Date();
    const todo: Todo = {
      id: this.nextId++,
      title: input.title.trim(),
      description: input.description?.trim() || '',
      completed: false,
      priority: input.priority || 'medium',
      dueDate: input.dueDate || null,
      tags: input.tags || [],
      createdAt: now,
      updatedAt: now,
    };

    this.todos.push(todo);
    return todo;
  }

  findById(id: number): Todo | undefined {
    return this.todos.find((todo) => todo.id === id);
  }

  findAll(filter?: TodoFilter): Todo[] {
    let result = [...this.todos];

    if (filter) {
      if (filter.completed !== undefined) {
        result = result.filter((t) => t.completed === filter.completed);
      }
      if (filter.priority) {
        result = result.filter((t) => t.priority === filter.priority);
      }
      if (filter.tag) {
        result = result.filter((t) => t.tags.includes(filter.tag!));
      }
      if (filter.dueBefore) {
        result = result.filter((t) => t.dueDate && t.dueDate < filter.dueBefore!);
      }
      if (filter.dueAfter) {
        result = result.filter((t) => t.dueDate && t.dueDate > filter.dueAfter!);
      }
    }

    return result;
  }

  update(id: number, input: UpdateTodoInput): Todo | undefined {
    const todo = this.findById(id);
    if (!todo) return undefined;

    if (input.title !== undefined) {
      validateTitle(input.title);
      todo.title = input.title.trim();
    }
    if (input.description !== undefined) {
      validateDescription(input.description);
      todo.description = input.description.trim();
    }
    if (input.priority !== undefined) {
      todo.priority = input.priority;
    }
    if (input.dueDate !== undefined) {
      todo.dueDate = input.dueDate;
    }
    if (input.tags !== undefined) {
      validateTags(input.tags);
      todo.tags = input.tags;
    }

    todo.updatedAt = new Date();
    return todo;
  }

  delete(id: number): boolean {
    const index = this.todos.findIndex((todo) => todo.id === id);
    if (index === -1) return false;
    this.todos.splice(index, 1);
    return true;
  }

  complete(id: number): Todo | undefined {
    const todo = this.findById(id);
    if (!todo) return undefined;
    todo.completed = true;
    todo.updatedAt = new Date();
    return todo;
  }

  uncomplete(id: number): Todo | undefined {
    const todo = this.findById(id);
    if (!todo) return undefined;
    todo.completed = false;
    todo.updatedAt = new Date();
    return todo;
  }

  getStats(): TodoStats {
    const stats: TodoStats = {
      total: this.todos.length,
      completed: 0,
      pending: 0,
      overdue: 0,
      byPriority: { low: 0, medium: 0, high: 0 },
    };

    for (const todo of this.todos) {
      if (todo.completed) {
        stats.completed++;
      } else {
        stats.pending++;
        if (todo.dueDate && isOverdue(todo.dueDate)) {
          stats.overdue++;
        }
      }
      stats.byPriority[todo.priority]++;
    }

    return stats;
  }

  getOverdue(): Todo[] {
    return this.todos.filter((todo) => !todo.completed && todo.dueDate && isOverdue(todo.dueDate));
  }

  getDueToday(): Todo[] {
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const tomorrow = addDays(today, 1);

    return this.todos.filter(
      (todo) => !todo.completed && todo.dueDate && todo.dueDate >= today && todo.dueDate < tomorrow
    );
  }

  getDueThisWeek(): Todo[] {
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const nextWeek = addDays(today, 7);

    return this.todos.filter(
      (todo) => !todo.completed && todo.dueDate && todo.dueDate >= today && todo.dueDate < nextWeek
    );
  }
}

// ============ Main Application ============
const todoService = new TodoService();

// Create some todos
todoService.create({
  title: 'Learn TypeScript modules',
  description: 'Complete the modules chapter',
  priority: 'high',
  dueDate: addDays(new Date(), 1),
  tags: ['learning', 'typescript'],
});

todoService.create({
  title: 'Refactor project',
  priority: 'medium',
  dueDate: addDays(new Date(), 3),
  tags: ['coding'],
});

todoService.create({
  title: 'Review pull requests',
  priority: 'low',
  tags: ['work'],
});

// Display todos
console.log('=== All Todos ===');
const todos = todoService.findAll();
todos.forEach((todo) => console.log(formatTodo(todo)));

console.log('\n=== Stats ===');
const stats = todoService.getStats();
console.log(`Total: ${stats.total}`);
console.log(`Completed: ${stats.completed}`);
console.log(`Pending: ${stats.pending}`);
console.log(`Overdue: ${stats.overdue}`);

This works, but has problems:

  • Hard to navigate (everything in one file)
  • Difficult to test individual parts
  • No clear boundaries between concerns
  • Cannot reuse parts in other projects

Target Structure

We will refactor into this structure:

todo-app/
├── src/
│   ├── index.ts              # Entry point
│   ├── models/
│   │   ├── todo.ts           # Todo interface and types
│   │   ├── filters.ts        # Filter and stats types
│   │   └── index.ts          # Barrel export
│   ├── services/
│   │   ├── todo-service.ts   # TodoService class
│   │   └── index.ts          # Barrel export
│   ├── utils/
│   │   ├── validation.ts     # Validation functions
│   │   ├── formatting.ts     # Formatting functions
│   │   ├── date-utils.ts     # Date utilities
│   │   └── index.ts          # Barrel export
│   └── types/
│       └── index.ts          # Shared type utilities
├── package.json
└── tsconfig.json

Step 1: Create the Models

Start with the data structures:

// src/models/todo.ts
/**
 * Priority levels for todos
 */
export type Priority = 'low' | 'medium' | 'high';

/**
 * A todo item
 */
export interface Todo {
  id: number;
  title: string;
  description: string;
  completed: boolean;
  priority: Priority;
  dueDate: Date | null;
  tags: string[];
  createdAt: Date;
  updatedAt: Date;
}

/**
 * Input for creating a new todo
 */
export interface CreateTodoInput {
  title: string;
  description?: string;
  priority?: Priority;
  dueDate?: Date;
  tags?: string[];
}

/**
 * Input for updating an existing todo
 */
export interface UpdateTodoInput {
  title?: string;
  description?: string;
  priority?: Priority;
  dueDate?: Date | null;
  tags?: string[];
}
// src/models/filters.ts
import type { Priority } from './todo';

/**
 * Filter options for querying todos
 */
export interface TodoFilter {
  completed?: boolean;
  priority?: Priority;
  tag?: string;
  dueBefore?: Date;
  dueAfter?: Date;
}

/**
 * Statistics about the todo list
 */
export interface TodoStats {
  total: number;
  completed: number;
  pending: number;
  overdue: number;
  byPriority: Record<Priority, number>;
}
// src/models/index.ts
export type { Todo, Priority, CreateTodoInput, UpdateTodoInput } from './todo';
export type { TodoFilter, TodoStats } from './filters';

Step 2: Create the Utilities

Extract utility functions into separate files:

// src/utils/validation.ts
/**
 * Validation error with a descriptive message
 */
export class ValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

/**
 * Validates a todo title
 * @throws ValidationError if invalid
 */
export function validateTitle(title: string): void {
  if (!title || title.trim().length === 0) {
    throw new ValidationError('Title is required');
  }
  if (title.length > 100) {
    throw new ValidationError('Title must be 100 characters or less');
  }
}

/**
 * Validates a todo description
 * @throws ValidationError if invalid
 */
export function validateDescription(description: string): void {
  if (description.length > 500) {
    throw new ValidationError('Description must be 500 characters or less');
  }
}

/**
 * Validates todo tags
 * @throws ValidationError if invalid
 */
export function validateTags(tags: string[]): void {
  if (tags.length > 10) {
    throw new ValidationError('Maximum 10 tags allowed');
  }
  for (const tag of tags) {
    if (tag.length > 20) {
      throw new ValidationError('Tag must be 20 characters or less');
    }
  }
}
// src/utils/date-utils.ts
/**
 * Checks if a date is in the past (overdue)
 */
export function isOverdue(date: Date): boolean {
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  return date < today;
}

/**
 * Calculates days until a target date
 * @returns Positive for future, negative for past, 0 for today
 */
export function daysUntil(date: Date): number {
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const target = new Date(date);
  target.setHours(0, 0, 0, 0);
  const diff = target.getTime() - today.getTime();
  return Math.ceil(diff / (1000 * 60 * 60 * 24));
}

/**
 * Adds days to a date
 * @returns New date with days added
 */
export function addDays(date: Date, days: number): Date {
  const result = new Date(date);
  result.setDate(result.getDate() + days);
  return result;
}

/**
 * Gets the start of today (midnight)
 */
export function startOfToday(): Date {
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  return today;
}

/**
 * Checks if a date is today
 */
export function isToday(date: Date): boolean {
  const today = startOfToday();
  const tomorrow = addDays(today, 1);
  return date >= today && date < tomorrow;
}
// src/utils/formatting.ts
import type { Priority, Todo } from '../models';

/**
 * Formats a date for display
 */
export function formatDate(date: Date): string {
  return date.toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'short',
    day: 'numeric',
  });
}

/**
 * Formats a priority as an icon
 */
export function formatPriority(priority: Priority): string {
  const icons: Record<Priority, string> = {
    low: '[ ]',
    medium: '[*]',
    high: '[!]',
  };
  return icons[priority];
}

/**
 * Formats a todo for display
 */
export function formatTodo(todo: Todo): string {
  const status = todo.completed ? '[x]' : '[ ]';
  const priority = formatPriority(todo.priority);
  const due = todo.dueDate ? ` (Due: ${formatDate(todo.dueDate)})` : '';
  const tags = todo.tags.length > 0 ? ` [${todo.tags.join(', ')}]` : '';
  return `${status} ${priority} ${todo.title}${due}${tags}`;
}

/**
 * Formats a todo list for display
 */
export function formatTodoList(todos: Todo[]): string {
  if (todos.length === 0) {
    return 'No todos found.';
  }
  return todos.map(formatTodo).join('\n');
}
// src/utils/index.ts
// Validation utilities
export { ValidationError, validateTitle, validateDescription, validateTags } from './validation';

// Date utilities
export { isOverdue, daysUntil, addDays, startOfToday, isToday } from './date-utils';

// Formatting utilities
export { formatDate, formatPriority, formatTodo, formatTodoList } from './formatting';

Step 3: Create the Service

The service contains business logic:

// src/services/todo-service.ts
import type { CreateTodoInput, Todo, TodoFilter, TodoStats, UpdateTodoInput } from '../models';
import {
  addDays,
  isOverdue,
  startOfToday,
  validateDescription,
  validateTags,
  validateTitle,
} from '../utils';

/**
 * Service for managing todos
 */
export class TodoService {
  private todos: Todo[] = [];
  private nextId = 1;

  /**
   * Creates a new todo
   */
  create(input: CreateTodoInput): Todo {
    validateTitle(input.title);
    if (input.description) {
      validateDescription(input.description);
    }
    if (input.tags) {
      validateTags(input.tags);
    }

    const now = new Date();
    const todo: Todo = {
      id: this.nextId++,
      title: input.title.trim(),
      description: input.description?.trim() || '',
      completed: false,
      priority: input.priority || 'medium',
      dueDate: input.dueDate || null,
      tags: input.tags || [],
      createdAt: now,
      updatedAt: now,
    };

    this.todos.push(todo);
    return todo;
  }

  /**
   * Finds a todo by ID
   */
  findById(id: number): Todo | undefined {
    return this.todos.find((todo) => todo.id === id);
  }

  /**
   * Finds all todos, optionally filtered
   */
  findAll(filter?: TodoFilter): Todo[] {
    let result = [...this.todos];

    if (filter) {
      if (filter.completed !== undefined) {
        result = result.filter((t) => t.completed === filter.completed);
      }
      if (filter.priority) {
        result = result.filter((t) => t.priority === filter.priority);
      }
      if (filter.tag) {
        result = result.filter((t) => t.tags.includes(filter.tag!));
      }
      if (filter.dueBefore) {
        result = result.filter((t) => t.dueDate && t.dueDate < filter.dueBefore!);
      }
      if (filter.dueAfter) {
        result = result.filter((t) => t.dueDate && t.dueDate > filter.dueAfter!);
      }
    }

    return result;
  }

  /**
   * Updates an existing todo
   */
  update(id: number, input: UpdateTodoInput): Todo | undefined {
    const todo = this.findById(id);
    if (!todo) return undefined;

    if (input.title !== undefined) {
      validateTitle(input.title);
      todo.title = input.title.trim();
    }
    if (input.description !== undefined) {
      validateDescription(input.description);
      todo.description = input.description.trim();
    }
    if (input.priority !== undefined) {
      todo.priority = input.priority;
    }
    if (input.dueDate !== undefined) {
      todo.dueDate = input.dueDate;
    }
    if (input.tags !== undefined) {
      validateTags(input.tags);
      todo.tags = input.tags;
    }

    todo.updatedAt = new Date();
    return todo;
  }

  /**
   * Deletes a todo by ID
   */
  delete(id: number): boolean {
    const index = this.todos.findIndex((todo) => todo.id === id);
    if (index === -1) return false;
    this.todos.splice(index, 1);
    return true;
  }

  /**
   * Marks a todo as completed
   */
  complete(id: number): Todo | undefined {
    const todo = this.findById(id);
    if (!todo) return undefined;
    todo.completed = true;
    todo.updatedAt = new Date();
    return todo;
  }

  /**
   * Marks a todo as not completed
   */
  uncomplete(id: number): Todo | undefined {
    const todo = this.findById(id);
    if (!todo) return undefined;
    todo.completed = false;
    todo.updatedAt = new Date();
    return todo;
  }

  /**
   * Gets statistics about the todo list
   */
  getStats(): TodoStats {
    const stats: TodoStats = {
      total: this.todos.length,
      completed: 0,
      pending: 0,
      overdue: 0,
      byPriority: { low: 0, medium: 0, high: 0 },
    };

    for (const todo of this.todos) {
      if (todo.completed) {
        stats.completed++;
      } else {
        stats.pending++;
        if (todo.dueDate && isOverdue(todo.dueDate)) {
          stats.overdue++;
        }
      }
      stats.byPriority[todo.priority]++;
    }

    return stats;
  }

  /**
   * Gets all overdue todos
   */
  getOverdue(): Todo[] {
    return this.todos.filter((todo) => !todo.completed && todo.dueDate && isOverdue(todo.dueDate));
  }

  /**
   * Gets todos due today
   */
  getDueToday(): Todo[] {
    const today = startOfToday();
    const tomorrow = addDays(today, 1);

    return this.todos.filter(
      (todo) => !todo.completed && todo.dueDate && todo.dueDate >= today && todo.dueDate < tomorrow
    );
  }

  /**
   * Gets todos due within the next 7 days
   */
  getDueThisWeek(): Todo[] {
    const today = startOfToday();
    const nextWeek = addDays(today, 7);

    return this.todos.filter(
      (todo) => !todo.completed && todo.dueDate && todo.dueDate >= today && todo.dueDate < nextWeek
    );
  }
}
// src/services/index.ts
export { TodoService } from './todo-service';

Step 4: Create the Entry Point

The main file ties everything together:

// src/index.ts
import { TodoService } from './services';
import { addDays, formatTodo, formatTodoList } from './utils';

// Initialize the service
const todoService = new TodoService();

// Create sample todos
console.log('Creating todos...\n');

todoService.create({
  title: 'Learn TypeScript modules',
  description: 'Complete the modules chapter',
  priority: 'high',
  dueDate: addDays(new Date(), 1),
  tags: ['learning', 'typescript'],
});

todoService.create({
  title: 'Refactor project',
  priority: 'medium',
  dueDate: addDays(new Date(), 3),
  tags: ['coding'],
});

todoService.create({
  title: 'Review pull requests',
  priority: 'low',
  tags: ['work'],
});

// Display all todos
console.log('=== All Todos ===');
const allTodos = todoService.findAll();
console.log(formatTodoList(allTodos));

// Display filtered todos
console.log('\n=== High Priority ===');
const highPriority = todoService.findAll({ priority: 'high' });
console.log(formatTodoList(highPriority));

// Display stats
console.log('\n=== Statistics ===');
const stats = todoService.getStats();
console.log(`Total: ${stats.total}`);
console.log(`Completed: ${stats.completed}`);
console.log(`Pending: ${stats.pending}`);
console.log(`Overdue: ${stats.overdue}`);
console.log(
  `By Priority: Low=${stats.byPriority.low}, Medium=${stats.byPriority.medium}, High=${stats.byPriority.high}`
);

// Complete a todo
console.log('\n=== Completing first todo ===');
todoService.complete(1);
const completed = todoService.findById(1);
if (completed) {
  console.log(formatTodo(completed));
}

// Show updated stats
console.log('\n=== Updated Statistics ===');
const updatedStats = todoService.getStats();
console.log(`Completed: ${updatedStats.completed}/${updatedStats.total}`);

Step 5: Configuration Files

// package.json
{
  "name": "todo-app",
  "version": "1.0.0",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "ts-node src/index.ts"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "ts-node": "^10.9.0"
  }
}
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Benefits of the Refactored Structure

1. Easy Navigation

Each concern has its own file:

  • Need to change validation? Go to utils/validation.ts
  • Need to add a new todo field? Go to models/todo.ts
  • Need to modify business logic? Go to services/todo-service.ts

2. Reusability

Utils can be used in other projects:

// In another project
import { formatDate, isOverdue } from './utils/date-utils';

3. Testability

Each module can be tested independently:

// validation.test.ts
import { ValidationError, validateTitle } from './utils/validation';

describe('validateTitle', () => {
  it('throws on empty title', () => {
    expect(() => validateTitle('')).toThrow(ValidationError);
  });

  it('throws on too long title', () => {
    const longTitle = 'a'.repeat(101);
    expect(() => validateTitle(longTitle)).toThrow(ValidationError);
  });

  it('accepts valid title', () => {
    expect(() => validateTitle('Valid Title')).not.toThrow();
  });
});

4. Clean Imports

Barrel exports make imports clean:

// Before refactoring
import { Todo } from "./models/todo";
import { TodoFilter } from "./models/filters";
import { formatTodo } from "./utils/formatting";
import { isOverdue } from "./utils/date-utils";

// After refactoring
import { Todo, TodoFilter } from "./models";
import { formatTodo, isOverdue } from "./utils";

5. Scalability

Adding new features is straightforward:

// Add a new tag service
services/
├── todo-service.ts
├── tag-service.ts    # New!
└── index.ts          # Add export

// Add new models
models/
├── todo.ts
├── filters.ts
├── tag.ts            # New!
└── index.ts          # Add export

Exercises

Exercise 1: Add a Tag Service

Create a TagService that manages tags across all todos:

// src/services/tag-service.ts
// Methods: getAllTags, getPopularTags, getTodosByTag

// Usage:
const tagService = new TagService(todoService);
tagService.getAllTags(); // ["learning", "typescript", "coding", "work"]
tagService.getPopularTags(2); // Most used 2 tags
tagService.getTodosByTag('work'); // Todos with "work" tag
Solution
// src/services/tag-service.ts
import type { Todo } from '../models';
import type { TodoService } from './todo-service';

export class TagService {
  constructor(private todoService: TodoService) {}

  /**
   * Gets all unique tags
   */
  getAllTags(): string[] {
    const todos = this.todoService.findAll();
    const tagSet = new Set<string>();

    for (const todo of todos) {
      for (const tag of todo.tags) {
        tagSet.add(tag);
      }
    }

    return Array.from(tagSet).sort();
  }

  /**
   * Gets the most popular tags
   */
  getPopularTags(limit: number): Array<{ tag: string; count: number }> {
    const todos = this.todoService.findAll();
    const tagCounts = new Map<string, number>();

    for (const todo of todos) {
      for (const tag of todo.tags) {
        tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
      }
    }

    return Array.from(tagCounts.entries())
      .map(([tag, count]) => ({ tag, count }))
      .sort((a, b) => b.count - a.count)
      .slice(0, limit);
  }

  /**
   * Gets all todos with a specific tag
   */
  getTodosByTag(tag: string): Todo[] {
    return this.todoService.findAll({ tag });
  }
}

// src/services/index.ts
export { TodoService } from './todo-service';
export { TagService } from './tag-service';

Exercise 2: Add Priority Utilities

Create priority-related utilities:

// src/utils/priority-utils.ts
// Functions: comparePriority, getNextPriority, getPreviousPriority

// Usage:
comparePriority('high', 'low'); // 1 (high > low)
comparePriority('low', 'high'); // -1 (low < high)
getNextPriority('low'); // "medium"
getNextPriority('high'); // "high" (already max)
getPreviousPriority('high'); // "medium"
Solution
// src/utils/priority-utils.ts
import type { Priority } from '../models';

const priorityOrder: Priority[] = ['low', 'medium', 'high'];

/**
 * Compares two priorities
 * @returns Positive if a > b, negative if a < b, 0 if equal
 */
export function comparePriority(a: Priority, b: Priority): number {
  return priorityOrder.indexOf(a) - priorityOrder.indexOf(b);
}

/**
 * Gets the next higher priority
 */
export function getNextPriority(priority: Priority): Priority {
  const index = priorityOrder.indexOf(priority);
  const nextIndex = Math.min(index + 1, priorityOrder.length - 1);
  return priorityOrder[nextIndex];
}

/**
 * Gets the previous lower priority
 */
export function getPreviousPriority(priority: Priority): Priority {
  const index = priorityOrder.indexOf(priority);
  const prevIndex = Math.max(index - 1, 0);
  return priorityOrder[prevIndex];
}

/**
 * Checks if a priority is the highest
 */
export function isHighestPriority(priority: Priority): boolean {
  return priority === 'high';
}

/**
 * Checks if a priority is the lowest
 */
export function isLowestPriority(priority: Priority): boolean {
  return priority === 'low';
}

// Update src/utils/index.ts
export {
  comparePriority,
  getNextPriority,
  getPreviousPriority,
  isHighestPriority,
  isLowestPriority,
} from './priority-utils';

Exercise 3: Add Sorting

Add a utility to sort todos:

// src/utils/sorting.ts
// Functions: sortByPriority, sortByDueDate, sortByCreatedAt

// Usage:
const sorted = sortByPriority(todos, 'desc'); // High priority first
const byDate = sortByDueDate(todos, 'asc'); // Earliest due first
Solution
// src/utils/sorting.ts
import type { Todo } from '../models';
import { comparePriority } from './priority-utils';

export type SortDirection = 'asc' | 'desc';

/**
 * Sorts todos by priority
 */
export function sortByPriority(todos: Todo[], direction: SortDirection = 'desc'): Todo[] {
  const sorted = [...todos].sort((a, b) => comparePriority(a.priority, b.priority));
  return direction === 'desc' ? sorted.reverse() : sorted;
}

/**
 * Sorts todos by due date (todos without due date go last)
 */
export function sortByDueDate(todos: Todo[], direction: SortDirection = 'asc'): Todo[] {
  return [...todos].sort((a, b) => {
    // Handle null due dates
    if (!a.dueDate && !b.dueDate) return 0;
    if (!a.dueDate) return 1; // a goes after b
    if (!b.dueDate) return -1; // a goes before b

    const diff = a.dueDate.getTime() - b.dueDate.getTime();
    return direction === 'asc' ? diff : -diff;
  });
}

/**
 * Sorts todos by creation date
 */
export function sortByCreatedAt(todos: Todo[], direction: SortDirection = 'desc'): Todo[] {
  return [...todos].sort((a, b) => {
    const diff = a.createdAt.getTime() - b.createdAt.getTime();
    return direction === 'asc' ? diff : -diff;
  });
}

/**
 * Sorts todos by multiple criteria
 */
export function sortTodos(
  todos: Todo[],
  criteria: Array<{ field: 'priority' | 'dueDate' | 'createdAt'; direction: SortDirection }>
): Todo[] {
  return [...todos].sort((a, b) => {
    for (const { field, direction } of criteria) {
      let diff = 0;

      switch (field) {
        case 'priority':
          diff = comparePriority(a.priority, b.priority);
          break;
        case 'dueDate':
          if (!a.dueDate && !b.dueDate) diff = 0;
          else if (!a.dueDate) diff = 1;
          else if (!b.dueDate) diff = -1;
          else diff = a.dueDate.getTime() - b.dueDate.getTime();
          break;
        case 'createdAt':
          diff = a.createdAt.getTime() - b.createdAt.getTime();
          break;
      }

      if (diff !== 0) {
        return direction === 'asc' ? diff : -diff;
      }
    }

    return 0;
  });
}

// Update src/utils/index.ts
export type { SortDirection } from './sorting';
export { sortByPriority, sortByDueDate, sortByCreatedAt, sortTodos } from './sorting';

Key Takeaways

  1. Start with models: Define your data structures first
  2. Extract utilities: Pure functions go in utils folder
  3. Services hold logic: Business logic in service classes
  4. Use barrel exports: Clean up imports with index files
  5. Keep files focused: Each file has one responsibility
  6. Document your code: Comments help future maintainers
  7. Refactoring is iterative: Do not try to get it perfect on the first pass

Resources

Resource Type Description
TypeScript Handbook: Modules Documentation Official guide
Clean Code TypeScript Guide Best practices
Refactoring Guru Tutorial Refactoring patterns

Module Complete

Congratulations! You have learned how to:

  • Import and export code between files
  • Structure projects professionally
  • Use barrel exports for clean APIs
  • Refactor single-file apps into modular structures

You are now ready to tackle the mini-projects in Module 7, where you will apply all your TypeScript knowledge to build complete applications.

Continue to Module 7: Mini-Projects