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
- Start with models: Define your data structures first
- Extract utilities: Pure functions go in utils folder
- Services hold logic: Business logic in service classes
- Use barrel exports: Clean up imports with index files
- Keep files focused: Each file has one responsibility
- Document your code: Comments help future maintainers
- 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.