Skip to main content

๐ŸŽฏ Mini-Project: Task Manager

Congratulations on completing all five TypeScript Fundamentals lessons! Now it's time to put everything together. In this mini-project, you'll build a comprehensive Task Manager application that demonstrates your mastery of TypeScript's type system. You'll use interfaces, type aliases, union types, generics, utility types, type guards, and functions - everything you've learned! ๐Ÿš€

๐ŸŽฏ Project Objectives

By completing this project, you will:

  • Design and implement a complete TypeScript type system for a real application
  • Use interfaces and type aliases to model complex data structures
  • Apply union types and discriminated unions for state management
  • Implement generic functions for reusable data operations
  • Leverage utility types for type transformations
  • Create custom type guards for runtime validation
  • Write type-safe functions with proper parameter and return types

Estimated Time: 90-120 minutes

Difficulty: Intermediate - applies all Module 1 concepts

๐Ÿ’ก What You'll Build

A complete Task Manager system with:

  • Task creation, updating, and deletion
  • Priority levels and status tracking
  • Task filtering and sorting
  • Statistics and analytics
  • Full TypeScript type safety throughout

๐Ÿ“‘ Project Sections

๐Ÿ“‹ Project Requirements

Before we dive into coding, let's understand what we're building. A good task manager needs to handle various types of tasks, track their status, and provide ways to organize and analyze them.

Core Requirements

Data Structure Requirements

  • Tasks must have: id, title, description, status, priority, created date, due date (optional), tags
  • Status can be: pending, in-progress, completed, cancelled
  • Priority can be: low, medium, high, urgent
  • Tags are strings that categorize tasks

Functionality Requirements

  • Create new tasks with all required fields
  • Update existing tasks (partial updates allowed)
  • Delete tasks by ID
  • Get a task by ID
  • Get all tasks
  • Filter tasks by status, priority, or tags
  • Sort tasks by different criteria
  • Calculate statistics (total, by status, by priority)
  • Validate task data

TypeScript Concepts to Apply

Concept Where to Use Example
Interfaces Task structure, Statistics interface Task { ... }
Type Aliases Status, Priority unions type Status = "pending" | "in-progress"
Union Types Status values, Filter options Status | Priority
Generics Filter functions, Sort functions function filter<T>(...)
Utility Types Partial updates, Required fields Partial<Task>
Type Guards Validation, Type checking isValidStatus(value)
Functions All operations Typed parameters & returns
graph TD A[Task Manager System] --> B[Data Types] A --> C[Operations] A --> D[Validation] B --> B1[Task Interface] B --> B2[Status Union] B --> B3[Priority Union] C --> C1[CRUD Operations] C --> C2[Filter & Sort] C --> C3[Statistics] D --> D1[Type Guards] D --> D2[Validation Functions] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style B fill:#764ba2,stroke:#333,stroke-width:2px,color:#fff style C fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff style D fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff

๐ŸŽจ Interactive: Task Data Structure

Explore the Task interface structure. Click on each property to see its type and purpose:

interface Task id: string title: string description: string status: Status priority: Priority createdAt: Date dueDate?: Date tags: string[] Click a property to learn more

โœ… Success Criteria

Your project is complete when:

  • All TypeScript code compiles without errors
  • All functions have proper type annotations
  • Type guards are used for runtime validation
  • Generic functions work with proper type inference
  • Utility types are used appropriately
  • All test cases pass

๐ŸŽจ Type System Design

Let's start by designing our type system. This is the foundation of our application - get this right, and everything else falls into place!

Step 1: Define Basic Types

Start with the simplest types and build up from there:

๐Ÿ‹๏ธ Task: Create Status and Priority Types

Create type aliases for Status and Priority using union types with literal values.

// TODO: Define Status type
// Should include: "pending", "in-progress", "completed", "cancelled"

// TODO: Define Priority type
// Should include: "low", "medium", "high", "urgent"

// Test your types:
const status1: Status = "pending";        // โœ… Should work
const status2: Status = "in-progress";    // โœ… Should work
// const status3: Status = "invalid";     // โŒ Should error

const priority1: Priority = "low";        // โœ… Should work
const priority2: Priority = "urgent";     // โœ… Should work
// const priority3: Priority = "invalid";  // โŒ Should error
๐Ÿ’ก Hint

Use the pipe operator | to create unions of string literal types. Remember: type MyType = "option1" | "option2" | "option3";

โœ… Solution
type Status = "pending" | "in-progress" | "completed" | "cancelled";
type Priority = "low" | "medium" | "high" | "urgent";

Step 2: Define the Task Interface

Now let's create the main Task interface:

๐Ÿ‹๏ธ Task: Create the Task Interface

Create an interface that represents a task with all necessary properties.

// TODO: Define Task interface
// Required properties:
// - id: string
// - title: string
// - description: string
// - status: Status (use the type you created)
// - priority: Priority (use the type you created)
// - createdAt: Date
// - tags: array of strings
// Optional properties:
// - dueDate: Date

// Test your interface:
const task: Task = {
    id: "task-1",
    title: "Learn TypeScript",
    description: "Complete all TypeScript lessons",
    status: "in-progress",
    priority: "high",
    createdAt: new Date(),
    tags: ["learning", "typescript"],
    dueDate: new Date("2024-12-31")
};
๐Ÿ’ก Hint

Use the interface keyword. Remember that optional properties use the ? modifier. Arrays can be typed as string[] or Array<string>.

โœ… Solution
interface Task {
    id: string;
    title: string;
    description: string;
    status: Status;
    priority: Priority;
    createdAt: Date;
    dueDate?: Date;
    tags: string[];
}

Step 3: Create Helper Types

We'll need some additional types for various operations:

๐Ÿ‹๏ธ Task: Create TaskInput and TaskUpdate Types

Create types for creating new tasks and updating existing ones.

// TODO: Define TaskInput type
// This should be the Task interface WITHOUT id and createdAt
// (since these are generated by the system)
// Hint: Use the Omit utility type

// TODO: Define TaskUpdate type
// This should allow partial updates to a Task
// (all fields optional except id)
// Hint: Use Partial and Pick utility types

// Test your types:
const newTask: TaskInput = {
    title: "New Task",
    description: "Task description",
    status: "pending",
    priority: "medium",
    tags: []
    // โœ… No id or createdAt needed
};

const taskUpdate: TaskUpdate = {
    id: "task-1",
    status: "completed"
    // โœ… Only updating status, other fields optional
};
๐Ÿ’ก Hint

Use Omit<Task, "id" | "createdAt"> for TaskInput. For TaskUpdate, combine Pick<Task, "id"> with Partial<Omit<Task, "id">> using intersection (&).

โœ… Solution
type TaskInput = Omit<Task, "id" | "createdAt">;

type TaskUpdate = Pick<Task, "id"> & Partial<Omit<Task, "id">>;

Step 4: Define Filter and Sort Types

๐Ÿ‹๏ธ Task: Create FilterOptions and SortField Types

// TODO: Define FilterOptions interface
// Optional properties for filtering:
// - status: Status
// - priority: Priority
// - tags: string array
// - completed: boolean (to filter completed/incomplete tasks)

// TODO: Define SortField type
// Union of fields we can sort by: "createdAt", "dueDate", "priority", "title"

// TODO: Define SortOrder type
// Union of: "asc" (ascending) or "desc" (descending)

// Test your types:
const filters: FilterOptions = {
    status: "in-progress",
    priority: "high"
};

const sortBy: SortField = "createdAt";
const sortOrder: SortOrder = "desc";
โœ… Solution
interface FilterOptions {
    status?: Status;
    priority?: Priority;
    tags?: string[];
    completed?: boolean;
}

type SortField = "createdAt" | "dueDate" | "priority" | "title";
type SortOrder = "asc" | "desc";

Step 5: Statistics Interface

๐Ÿ‹๏ธ Task: Create TaskStatistics Interface

// TODO: Define TaskStatistics interface
// Properties:
// - total: number (total tasks)
// - byStatus: Record of Status to number
// - byPriority: Record of Priority to number
// - completed: number
// - overdue: number
// Hint: Use Record<Status, number> for byStatus

// Test your interface:
const stats: TaskStatistics = {
    total: 10,
    byStatus: {
        "pending": 3,
        "in-progress": 4,
        "completed": 2,
        "cancelled": 1
    },
    byPriority: {
        "low": 2,
        "medium": 5,
        "high": 2,
        "urgent": 1
    },
    completed: 2,
    overdue: 1
};
โœ… Solution
interface TaskStatistics {
    total: number;
    byStatus: Record<Status, number>;
    byPriority: Record<Priority, number>;
    completed: number;
    overdue: number;
}

โœ… Type System Complete!

Great work! You've designed a complete type system for the Task Manager. You've used:

  • Type aliases with union types (Status, Priority)
  • Interfaces (Task, FilterOptions, TaskStatistics)
  • Utility types (Omit, Partial, Pick, Record)
  • Optional properties
  • Type composition with intersection types

โš™๏ธ Core Functionality

Now that we have our type system in place, let's implement the core functionality. We'll create type guards, validation functions, and the main CRUD operations.

Step 1: Type Guards and Validation

Type guards help us validate data at runtime. Let's create them for our Status and Priority types:

๐Ÿ‹๏ธ Task: Create Type Guards

Create type guard functions to validate Status and Priority values at runtime.

// TODO: Create isValidStatus type guard
// Function signature: function isValidStatus(value: any): value is Status
// Should return true if value is a valid Status

// TODO: Create isValidPriority type guard
// Function signature: function isValidPriority(value: any): value is Priority
// Should return true if value is a valid Priority

// Test your type guards:
console.log(isValidStatus("pending"));      // โœ… true
console.log(isValidStatus("invalid"));      // โŒ false
console.log(isValidPriority("high"));       // โœ… true
console.log(isValidPriority("invalid"));    // โŒ false
๐Ÿ’ก Hint

Use an array of valid values and check if the input value is included. Remember to use the type predicate syntax: value is Status in the return type.

โœ… Solution
function isValidStatus(value: any): value is Status {
    return ["pending", "in-progress", "completed", "cancelled"].includes(value);
}

function isValidPriority(value: any): value is Priority {
    return ["low", "medium", "high", "urgent"].includes(value);
}

๐Ÿ‹๏ธ Task: Create Task Validation Function

Create a function to validate a complete Task object.

// TODO: Create validateTask function
// Function signature: function validateTask(task: Task): boolean
// Should check:
// - id is not empty
// - title is not empty
// - description is not empty
// - status is valid (use isValidStatus)
// - priority is valid (use isValidPriority)
// - createdAt is a valid Date
// - tags is an array
// Return true if all checks pass, false otherwise

// Test your validation:
const validTask: Task = {
    id: "task-1",
    title: "Test Task",
    description: "Test Description",
    status: "pending",
    priority: "high",
    createdAt: new Date(),
    tags: ["test"]
};

console.log(validateTask(validTask));  // โœ… true

const invalidTask: Task = {
    id: "",
    title: "",
    description: "Test",
    status: "invalid" as Status,
    priority: "high",
    createdAt: new Date(),
    tags: []
};

console.log(validateTask(invalidTask));  // โŒ false
๐Ÿ’ก Hint

Check each property one by one. Use && to combine all conditions. For dates, check instanceof Date and !isNaN(date.getTime()).

โœ… Solution
function validateTask(task: Task): boolean {
    return (
        task.id.trim().length > 0 &&
        task.title.trim().length > 0 &&
        task.description.trim().length > 0 &&
        isValidStatus(task.status) &&
        isValidPriority(task.priority) &&
        task.createdAt instanceof Date &&
        !isNaN(task.createdAt.getTime()) &&
        Array.isArray(task.tags)
    );
}

Step 2: Task Manager Class

Let's create a class to manage our tasks. This will hold all our CRUD operations:

๐Ÿ‹๏ธ Task: Create TaskManager Class Structure

Create a class with a private tasks array and basic structure.

// TODO: Create TaskManager class
// Private property: tasks (array of Task objects)
// Constructor: initializes empty tasks array
// Private method: generateId() - generates unique IDs (use Date.now() + random number)

class TaskManager {
    // TODO: Add private tasks property
    
    // TODO: Add constructor
    
    // TODO: Add private generateId method
}

// Test your class:
const manager = new TaskManager();
console.log(manager);  // Should create instance successfully
โœ… Solution
class TaskManager {
    private tasks: Task[] = [];

    constructor() {
        this.tasks = [];
    }

    private generateId(): string {
        return `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
    }
}

Step 3: Create Task Operation

๐Ÿ‹๏ธ Task: Implement createTask Method

Add a method to create new tasks.

// TODO: Add createTask method to TaskManager class
// Method signature: createTask(input: TaskInput): Task
// Should:
// 1. Generate a unique id
// 2. Set createdAt to current Date
// 3. Create a Task object with all properties
// 4. Validate the task (throw error if invalid)
// 5. Add to tasks array
// 6. Return the created task

// Add this method to your TaskManager class:
class TaskManager {
    // ... previous code ...

    createTask(input: TaskInput): Task {
        // TODO: Implement this method
    }
}

// Test your method:
const manager = new TaskManager();
const newTask = manager.createTask({
    title: "Learn TypeScript",
    description: "Complete all lessons",
    status: "in-progress",
    priority: "high",
    tags: ["learning"]
});
console.log(newTask);  // Should show task with generated id and createdAt
๐Ÿ’ก Hint

Create the task object using the spread operator: { ...input, id: this.generateId(), createdAt: new Date() }. Don't forget to validate before adding to the array!

โœ… Solution
createTask(input: TaskInput): Task {
    const task: Task = {
        ...input,
        id: this.generateId(),
        createdAt: new Date()
    };

    if (!validateTask(task)) {
        throw new Error("Invalid task data");
    }

    this.tasks.push(task);
    return task;
}

Step 4: Read Operations

๐Ÿ‹๏ธ Task: Implement Read Methods

Add methods to retrieve tasks.

// TODO: Add these methods to TaskManager class

// Method 1: getTaskById(id: string): Task | undefined
// Should return the task with matching id, or undefined if not found

// Method 2: getAllTasks(): Task[]
// Should return a copy of all tasks (use spread or slice to avoid mutation)

// Method 3: getTaskCount(): number
// Should return the total number of tasks

// Add to your TaskManager class:
class TaskManager {
    // ... previous code ...

    getTaskById(id: string): Task | undefined {
        // TODO: Implement this method
    }

    getAllTasks(): Task[] {
        // TODO: Implement this method
    }

    getTaskCount(): number {
        // TODO: Implement this method
    }
}

// Test your methods:
const task = manager.getTaskById(newTask.id);
console.log(task);  // Should return the task

const allTasks = manager.getAllTasks();
console.log(allTasks);  // Should return array of all tasks

const count = manager.getTaskCount();
console.log(count);  // Should return number of tasks
๐Ÿ’ก Hint

Use find() for getTaskById. For getAllTasks, use the spread operator [...this.tasks] to return a copy. For getTaskCount, return this.tasks.length.

โœ… Solution
getTaskById(id: string): Task | undefined {
    return this.tasks.find(task => task.id === id);
}

getAllTasks(): Task[] {
    return [...this.tasks];
}

getTaskCount(): number {
    return this.tasks.length;
}

Step 5: Update Operation

๐Ÿ‹๏ธ Task: Implement updateTask Method

Add a method to update existing tasks with partial data.

// TODO: Add updateTask method to TaskManager class
// Method signature: updateTask(update: TaskUpdate): Task | undefined
// Should:
// 1. Find the task by id
// 2. If not found, return undefined
// 3. Merge the update with existing task (use spread operator)
// 4. Validate the updated task
// 5. Replace the old task with updated task
// 6. Return the updated task

// Add to your TaskManager class:
class TaskManager {
    // ... previous code ...

    updateTask(update: TaskUpdate): Task | undefined {
        // TODO: Implement this method
    }
}

// Test your method:
const updated = manager.updateTask({
    id: newTask.id,
    status: "completed",
    priority: "medium"
});
console.log(updated);  // Should show task with updated fields
๐Ÿ’ก Hint

Use findIndex() to get the task position. Merge with: { ...existingTask, ...update }. Remember to omit the id from the update spread!

โœ… Solution
updateTask(update: TaskUpdate): Task | undefined {
    const index = this.tasks.findIndex(task => task.id === update.id);
    
    if (index === -1) {
        return undefined;
    }

    const { id, ...updates } = update;
    const updatedTask: Task = {
        ...this.tasks[index],
        ...updates
    };

    if (!validateTask(updatedTask)) {
        throw new Error("Invalid task data");
    }

    this.tasks[index] = updatedTask;
    return updatedTask;
}

Step 6: Delete Operation

๐Ÿ‹๏ธ Task: Implement deleteTask Method

Add a method to delete tasks by ID.

// TODO: Add deleteTask method to TaskManager class
// Method signature: deleteTask(id: string): boolean
// Should:
// 1. Find the task by id
// 2. If found, remove it from the array
// 3. Return true if deleted, false if not found

// Add to your TaskManager class:
class TaskManager {
    // ... previous code ...

    deleteTask(id: string): boolean {
        // TODO: Implement this method
    }
}

// Test your method:
const deleted = manager.deleteTask(newTask.id);
console.log(deleted);  // Should return true

const deletedAgain = manager.deleteTask(newTask.id);
console.log(deletedAgain);  // Should return false (already deleted)
๐Ÿ’ก Hint

Use findIndex() to locate the task. If found (index !== -1), use splice() to remove it and return true. Otherwise return false.

โœ… Solution
deleteTask(id: string): boolean {
    const index = this.tasks.findIndex(task => task.id === id);
    
    if (index === -1) {
        return false;
    }

    this.tasks.splice(index, 1);
    return true;
}

โœ… Core CRUD Operations Complete!

Excellent work! You've implemented all the basic CRUD operations:

  • Create: createTask() with validation
  • Read: getTaskById(), getAllTasks(), getTaskCount()
  • Update: updateTask() with partial updates using Partial<T>
  • Delete: deleteTask() with boolean return

You've used type guards, utility types (Partial, Omit), proper return types, and error handling!

graph LR A[TaskManager] --> B[Create] A --> C[Read] A --> D[Update] A --> E[Delete] B --> B1[createTask] C --> C1[getTaskById] C --> C2[getAllTasks] C --> C3[getTaskCount] D --> D1[updateTask] E --> E1[deleteTask] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style B fill:#4CAF50,stroke:#333,stroke-width:2px style C fill:#2196F3,stroke:#333,stroke-width:2px style D fill:#FF9800,stroke:#333,stroke-width:2px style E fill:#f44336,stroke:#333,stroke-width:2px

๐ŸŽจ Interactive: CRUD Operations Simulator

Watch how each CRUD operation works with tasks. Click buttons to simulate operations:

Click an operation to see it in action

๐Ÿš€ Advanced Features

Now let's implement the advanced features: filtering, sorting, and statistics. This is where generics and advanced TypeScript features really shine!

Step 1: Filter Tasks

๐Ÿ‹๏ธ Task: Implement filterTasks Method

Create a method to filter tasks based on various criteria.

// TODO: Add filterTasks method to TaskManager class
// Method signature: filterTasks(filters: FilterOptions): Task[]
// Should filter tasks based on:
// - status (if provided)
// - priority (if provided)
// - tags (if provided - task must have ALL specified tags)
// - completed (if provided - true returns only completed, false returns non-completed)
// Return array of matching tasks

// Add to your TaskManager class:
class TaskManager {
    // ... previous code ...

    filterTasks(filters: FilterOptions): Task[] {
        // TODO: Implement this method
    }
}

// Test your method:
const highPriorityTasks = manager.filterTasks({ priority: "high" });
console.log(highPriorityTasks);

const inProgressTasks = manager.filterTasks({ status: "in-progress" });
console.log(inProgressTasks);

const completedHighPriority = manager.filterTasks({ 
    status: "completed", 
    priority: "high" 
});
console.log(completedHighPriority);
๐Ÿ’ก Hint

Use filter() method on this.tasks. For each filter option, check if it's defined before applying. For tags, use every() to check if all specified tags exist in the task's tags array.

โœ… Solution
filterTasks(filters: FilterOptions): Task[] {
    return this.tasks.filter(task => {
        // Filter by status
        if (filters.status && task.status !== filters.status) {
            return false;
        }

        // Filter by priority
        if (filters.priority && task.priority !== filters.priority) {
            return false;
        }

        // Filter by tags (task must have ALL specified tags)
        if (filters.tags && filters.tags.length > 0) {
            const hasAllTags = filters.tags.every(tag => 
                task.tags.includes(tag)
            );
            if (!hasAllTags) {
                return false;
            }
        }

        // Filter by completed status
        if (filters.completed !== undefined) {
            const isCompleted = task.status === "completed";
            if (isCompleted !== filters.completed) {
                return false;
            }
        }

        return true;
    });
}

Step 2: Sort Tasks

๐Ÿ‹๏ธ Task: Implement sortTasks Method

Create a method to sort tasks by different fields.

// TODO: Add sortTasks method to TaskManager class
// Method signature: sortTasks(tasks: Task[], field: SortField, order: SortOrder = "asc"): Task[]
// Should:
// 1. Sort tasks by the specified field
// 2. Handle ascending and descending order
// 3. Handle Date fields (createdAt, dueDate)
// 4. Handle string fields (title)
// 5. Handle priority (treat as: low < medium < high < urgent)
// 6. Return a NEW sorted array (don't mutate original)

// Add to your TaskManager class:
class TaskManager {
    // ... previous code ...

    sortTasks(tasks: Task[], field: SortField, order: SortOrder = "asc"): Task[] {
        // TODO: Implement this method
        // Hint: Create a copy first with [...tasks]
        // Hint: For priority, create a priority order map
    }
}

// Test your method:
const allTasks = manager.getAllTasks();
const sortedByDate = manager.sortTasks(allTasks, "createdAt", "desc");
console.log(sortedByDate);

const sortedByPriority = manager.sortTasks(allTasks, "priority", "desc");
console.log(sortedByPriority);
๐Ÿ’ก Hint

Use [...tasks].sort() to create a copy and sort. For priority, create an object mapping: { low: 0, medium: 1, high: 2, urgent: 3 }. Use localeCompare() for strings.

โœ… Solution
sortTasks(tasks: Task[], field: SortField, order: SortOrder = "asc"): Task[] {
    const priorityOrder: Record<Priority, number> = {
        "low": 0,
        "medium": 1,
        "high": 2,
        "urgent": 3
    };

    const sortedTasks = [...tasks].sort((a, b) => {
        let comparison = 0;

        switch (field) {
            case "createdAt":
            case "dueDate":
                const dateA = field === "createdAt" ? a.createdAt : (a.dueDate || new Date(0));
                const dateB = field === "createdAt" ? b.createdAt : (b.dueDate || new Date(0));
                comparison = dateA.getTime() - dateB.getTime();
                break;

            case "priority":
                comparison = priorityOrder[a.priority] - priorityOrder[b.priority];
                break;

            case "title":
                comparison = a.title.localeCompare(b.title);
                break;
        }

        return order === "asc" ? comparison : -comparison;
    });

    return sortedTasks;
}

Step 3: Calculate Statistics

๐Ÿ‹๏ธ Task: Implement getStatistics Method

Create a method to calculate task statistics.

// TODO: Add getStatistics method to TaskManager class
// Method signature: getStatistics(): TaskStatistics
// Should calculate:
// - total: total number of tasks
// - byStatus: count of tasks for each status
// - byPriority: count of tasks for each priority
// - completed: number of completed tasks
// - overdue: number of tasks past their due date (if dueDate exists and is past)

// Add to your TaskManager class:
class TaskManager {
    // ... previous code ...

    getStatistics(): TaskStatistics {
        // TODO: Implement this method
        // Hint: Use reduce() to build the byStatus and byPriority objects
    }
}

// Test your method:
const stats = manager.getStatistics();
console.log(stats);
// Should show object with all statistics
๐Ÿ’ก Hint

Initialize byStatus and byPriority with all possible values set to 0. Use reduce() or a loop to count. For overdue, check if task.dueDate && task.dueDate < new Date() && task.status !== "completed".

โœ… Solution
getStatistics(): TaskStatistics {
    const now = new Date();

    const byStatus: Record<Status, number> = {
        "pending": 0,
        "in-progress": 0,
        "completed": 0,
        "cancelled": 0
    };

    const byPriority: Record<Priority, number> = {
        "low": 0,
        "medium": 0,
        "high": 0,
        "urgent": 0
    };

    let completed = 0;
    let overdue = 0;

    this.tasks.forEach(task => {
        byStatus[task.status]++;
        byPriority[task.priority]++;

        if (task.status === "completed") {
            completed++;
        }

        if (
            task.dueDate &&
            task.dueDate < now &&
            task.status !== "completed" &&
            task.status !== "cancelled"
        ) {
            overdue++;
        }
    });

    return {
        total: this.tasks.length,
        byStatus,
        byPriority,
        completed,
        overdue
    };
}

Step 4: Search Tasks (Bonus Generic Function)

๐Ÿ‹๏ธ Task: Implement Generic Search Method

Create a generic search method that can search any field.

// TODO: Add searchTasks method to TaskManager class
// Method signature: searchTasks<K extends keyof Task>(field: K, value: Task[K]): Task[]
// This is a GENERIC method that can search any field of a task
// Should return tasks where the field matches the value

// Add to your TaskManager class:
class TaskManager {
    // ... previous code ...

    searchTasks<K extends keyof Task>(field: K, value: Task[K]): Task[] {
        // TODO: Implement this method
    }
}

// Test your method:
const tasksByTitle = manager.searchTasks("title", "Learn TypeScript");
console.log(tasksByTitle);

const tasksByStatus = manager.searchTasks("status", "in-progress");
console.log(tasksByStatus);

const tasksByPriority = manager.searchTasks("priority", "high");
console.log(tasksByPriority);
๐Ÿ’ก Hint

This is a generic method! Use filter() and compare task[field] with value. The generic constraint K extends keyof Task ensures field is a valid Task property.

โœ… Solution
searchTasks<K extends keyof Task>(field: K, value: Task[K]): Task[] {
    return this.tasks.filter(task => task[field] === value);
}

โœ… Advanced Features Complete!

Outstanding! You've implemented all advanced features:

  • Filtering: Complex multi-criteria filtering
  • Sorting: Sort by multiple fields with ascending/descending order
  • Statistics: Comprehensive analytics with Record types
  • Generic Search: Type-safe search using generics with keyof

You've mastered: Generics, Record types, keyof operator, and complex array operations!

๐ŸŽจ Interactive: Statistics Dashboard

See how the getStatistics() method calculates task metrics. Click to add sample tasks and watch the stats update:

TaskStatistics Dashboard Total Tasks 0 Completed 0 Overdue 0 byStatus: Record<Status, number> pending: 0 in-progress: 0 completed: 0 cancelled: 0 byPriority: Record<Priority, number> 0 low 0 medium 0 high 0 urgent

๐Ÿงช Testing & Validation

Let's test our Task Manager to make sure everything works correctly. We'll create comprehensive test cases for all functionality.

๐Ÿ‹๏ธ Task: Create Test Suite

Create a function that runs all tests and reports results.

// TODO: Create a test function
function testTaskManager(): void {
    console.log("๐Ÿงช Starting Task Manager Tests...\n");

    const manager = new TaskManager();

    // Test 1: Create tasks
    console.log("Test 1: Creating tasks...");
    const task1 = manager.createTask({
        title: "Learn TypeScript Basics",
        description: "Complete lessons 1-3",
        status: "completed",
        priority: "high",
        tags: ["learning", "typescript"]
    });
    console.log("โœ… Task 1 created:", task1.id);

    const task2 = manager.createTask({
        title: "Build Task Manager",
        description: "Complete mini-project",
        status: "in-progress",
        priority: "urgent",
        tags: ["project", "typescript"],
        dueDate: new Date("2024-12-31")
    });
    console.log("โœ… Task 2 created:", task2.id);

    const task3 = manager.createTask({
        title: "Review React Concepts",
        description: "Prepare for Module 2",
        status: "pending",
        priority: "medium",
        tags: ["learning", "react"]
    });
    console.log("โœ… Task 3 created:", task3.id);

    // Test 2: Get tasks
    console.log("\nTest 2: Retrieving tasks...");
    const allTasks = manager.getAllTasks();
    console.log(`โœ… Total tasks: ${allTasks.length}`);

    const foundTask = manager.getTaskById(task1.id);
    console.log(`โœ… Found task: ${foundTask?.title}`);

    // Test 3: Update task
    console.log("\nTest 3: Updating task...");
    const updated = manager.updateTask({
        id: task3.id,
        status: "in-progress",
        priority: "high"
    });
    console.log(`โœ… Updated task: ${updated?.title} - Status: ${updated?.status}`);

    // Test 4: Filter tasks
    console.log("\nTest 4: Filtering tasks...");
    const highPriority = manager.filterTasks({ priority: "high" });
    console.log(`โœ… High priority tasks: ${highPriority.length}`);

    const inProgress = manager.filterTasks({ status: "in-progress" });
    console.log(`โœ… In-progress tasks: ${inProgress.length}`);

    const learningTasks = manager.filterTasks({ tags: ["learning"] });
    console.log(`โœ… Learning tasks: ${learningTasks.length}`);

    // Test 5: Sort tasks
    console.log("\nTest 5: Sorting tasks...");
    const sortedByPriority = manager.sortTasks(allTasks, "priority", "desc");
    console.log(`โœ… Sorted by priority (desc):`, 
        sortedByPriority.map(t => `${t.title} (${t.priority})`).join(", "));

    // Test 6: Statistics
    console.log("\nTest 6: Getting statistics...");
    const stats = manager.getStatistics();
    console.log("โœ… Statistics:");
    console.log(`   Total: ${stats.total}`);
    console.log(`   Completed: ${stats.completed}`);
    console.log(`   By Status:`, stats.byStatus);
    console.log(`   By Priority:`, stats.byPriority);

    // Test 7: Generic search
    console.log("\nTest 7: Generic search...");
    const urgentTasks = manager.searchTasks("priority", "urgent");
    console.log(`โœ… Urgent tasks: ${urgentTasks.length}`);

    // Test 8: Delete task
    console.log("\nTest 8: Deleting task...");
    const deleted = manager.deleteTask(task1.id);
    console.log(`โœ… Task deleted: ${deleted}`);
    console.log(`โœ… Remaining tasks: ${manager.getTaskCount()}`);

    console.log("\n๐ŸŽ‰ All tests completed!");
}

// Run the tests
testTaskManager();
โœ… Expected Output
๐Ÿงช Starting Task Manager Tests...

Test 1: Creating tasks...
โœ… Task 1 created: task-1234567890-abc123
โœ… Task 2 created: task-1234567891-def456
โœ… Task 3 created: task-1234567892-ghi789

Test 2: Retrieving tasks...
โœ… Total tasks: 3
โœ… Found task: Learn TypeScript Basics

Test 3: Updating task...
โœ… Updated task: Review React Concepts - Status: in-progress

Test 4: Filtering tasks...
โœ… High priority tasks: 2
โœ… In-progress tasks: 2
โœ… Learning tasks: 2

Test 5: Sorting tasks...
โœ… Sorted by priority (desc): Build Task Manager (urgent), Learn TypeScript Basics (high), Review React Concepts (high)

Test 6: Getting statistics...
โœ… Statistics:
   Total: 3
   Completed: 1
   By Status: { pending: 0, 'in-progress': 2, completed: 1, cancelled: 0 }
   By Priority: { low: 0, medium: 0, high: 2, urgent: 1 }

Test 7: Generic search...
โœ… Urgent tasks: 1

Test 8: Deleting task...
โœ… Task deleted: true
โœ… Remaining tasks: 2

๐ŸŽ‰ All tests completed!

โš ๏ธ Common Issues to Check

  • Type errors: Make sure all functions have proper type annotations
  • Validation: Ensure tasks are validated before being added/updated
  • Immutability: Array methods like sort() should return new arrays, not mutate originals
  • Edge cases: Handle missing tasks (undefined returns), empty arrays, etc.
  • Date handling: Make sure dates are properly compared for overdue tasks

๐Ÿ’ก Complete Solution

Here's the complete Task Manager implementation with all features. Use this to check your work or if you get stuck!

๐Ÿ“ฆ Click to View Complete Solution
// ============================================
// TYPE DEFINITIONS
// ============================================

type Status = "pending" | "in-progress" | "completed" | "cancelled";
type Priority = "low" | "medium" | "high" | "urgent";

interface Task {
    id: string;
    title: string;
    description: string;
    status: Status;
    priority: Priority;
    createdAt: Date;
    dueDate?: Date;
    tags: string[];
}

type TaskInput = Omit<Task, "id" | "createdAt">;
type TaskUpdate = Pick<Task, "id"> & Partial<Omit<Task, "id">>;

interface FilterOptions {
    status?: Status;
    priority?: Priority;
    tags?: string[];
    completed?: boolean;
}

type SortField = "createdAt" | "dueDate" | "priority" | "title";
type SortOrder = "asc" | "desc";

interface TaskStatistics {
    total: number;
    byStatus: Record<Status, number>;
    byPriority: Record<Priority, number>;
    completed: number;
    overdue: number;
}

// ============================================
// TYPE GUARDS & VALIDATION
// ============================================

function isValidStatus(value: any): value is Status {
    return ["pending", "in-progress", "completed", "cancelled"].includes(value);
}

function isValidPriority(value: any): value is Priority {
    return ["low", "medium", "high", "urgent"].includes(value);
}

function validateTask(task: Task): boolean {
    return (
        task.id.trim().length > 0 &&
        task.title.trim().length > 0 &&
        task.description.trim().length > 0 &&
        isValidStatus(task.status) &&
        isValidPriority(task.priority) &&
        task.createdAt instanceof Date &&
        !isNaN(task.createdAt.getTime()) &&
        Array.isArray(task.tags)
    );
}

// ============================================
// TASK MANAGER CLASS
// ============================================

class TaskManager {
    private tasks: Task[] = [];

    constructor() {
        this.tasks = [];
    }

    private generateId(): string {
        return `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
    }

    // CREATE
    createTask(input: TaskInput): Task {
        const task: Task = {
            ...input,
            id: this.generateId(),
            createdAt: new Date()
        };

        if (!validateTask(task)) {
            throw new Error("Invalid task data");
        }

        this.tasks.push(task);
        return task;
    }

    // READ
    getTaskById(id: string): Task | undefined {
        return this.tasks.find(task => task.id === id);
    }

    getAllTasks(): Task[] {
        return [...this.tasks];
    }

    getTaskCount(): number {
        return this.tasks.length;
    }

    // UPDATE
    updateTask(update: TaskUpdate): Task | undefined {
        const index = this.tasks.findIndex(task => task.id === update.id);
        
        if (index === -1) {
            return undefined;
        }

        const { id, ...updates } = update;
        const updatedTask: Task = {
            ...this.tasks[index],
            ...updates
        };

        if (!validateTask(updatedTask)) {
            throw new Error("Invalid task data");
        }

        this.tasks[index] = updatedTask;
        return updatedTask;
    }

    // DELETE
    deleteTask(id: string): boolean {
        const index = this.tasks.findIndex(task => task.id === id);
        
        if (index === -1) {
            return false;
        }

        this.tasks.splice(index, 1);
        return true;
    }

    // FILTER
    filterTasks(filters: FilterOptions): Task[] {
        return this.tasks.filter(task => {
            if (filters.status && task.status !== filters.status) {
                return false;
            }

            if (filters.priority && task.priority !== filters.priority) {
                return false;
            }

            if (filters.tags && filters.tags.length > 0) {
                const hasAllTags = filters.tags.every(tag => 
                    task.tags.includes(tag)
                );
                if (!hasAllTags) {
                    return false;
                }
            }

            if (filters.completed !== undefined) {
                const isCompleted = task.status === "completed";
                if (isCompleted !== filters.completed) {
                    return false;
                }
            }

            return true;
        });
    }

    // SORT
    sortTasks(tasks: Task[], field: SortField, order: SortOrder = "asc"): Task[] {
        const priorityOrder: Record<Priority, number> = {
            "low": 0,
            "medium": 1,
            "high": 2,
            "urgent": 3
        };

        const sortedTasks = [...tasks].sort((a, b) => {
            let comparison = 0;

            switch (field) {
                case "createdAt":
                case "dueDate":
                    const dateA = field === "createdAt" ? a.createdAt : (a.dueDate || new Date(0));
                    const dateB = field === "createdAt" ? b.createdAt : (b.dueDate || new Date(0));
                    comparison = dateA.getTime() - dateB.getTime();
                    break;

                case "priority":
                    comparison = priorityOrder[a.priority] - priorityOrder[b.priority];
                    break;

                case "title":
                    comparison = a.title.localeCompare(b.title);
                    break;
            }

            return order === "asc" ? comparison : -comparison;
        });

        return sortedTasks;
    }

    // STATISTICS
    getStatistics(): TaskStatistics {
        const now = new Date();

        const byStatus: Record<Status, number> = {
            "pending": 0,
            "in-progress": 0,
            "completed": 0,
            "cancelled": 0
        };

        const byPriority: Record<Priority, number> = {
            "low": 0,
            "medium": 0,
            "high": 0,
            "urgent": 0
        };

        let completed = 0;
        let overdue = 0;

        this.tasks.forEach(task => {
            byStatus[task.status]++;
            byPriority[task.priority]++;

            if (task.status === "completed") {
                completed++;
            }

            if (
                task.dueDate &&
                task.dueDate < now &&
                task.status !== "completed" &&
                task.status !== "cancelled"
            ) {
                overdue++;
            }
        });

        return {
            total: this.tasks.length,
            byStatus,
            byPriority,
            completed,
            overdue
        };
    }

    // GENERIC SEARCH
    searchTasks<K extends keyof Task>(field: K, value: Task[K]): Task[] {
        return this.tasks.filter(task => task[field] === value);
    }
}

// ============================================
// EXPORT (for use in other files)
// ============================================

// If using modules, export everything
// export { Task, TaskInput, TaskUpdate, Status, Priority, FilterOptions, SortField, SortOrder, TaskStatistics, TaskManager };

โœ… What You've Accomplished

In this project, you successfully applied:

  • โœ… Type Aliases - Status, Priority, SortField, SortOrder
  • โœ… Interfaces - Task, FilterOptions, TaskStatistics
  • โœ… Union Types - Literal string unions for Status and Priority
  • โœ… Utility Types - Omit, Partial, Pick, Record
  • โœ… Type Guards - isValidStatus, isValidPriority, custom validation
  • โœ… Generics - Generic search method with keyof
  • โœ… Optional Properties - dueDate in Task
  • โœ… Type-safe Functions - All parameters and returns properly typed
  • โœ… Array Methods - filter, sort, map, find, every
  • โœ… Classes - TaskManager with private properties and methods

๐ŸŽฏ Challenges & Extensions

Ready to take your Task Manager to the next level? Try these challenges!

๐ŸŒŸ Challenge 1: Task Dependencies

Goal: Add support for task dependencies - tasks that must be completed before others can start.

  • Add a dependsOn property (array of task IDs)
  • Create a method to check if a task's dependencies are satisfied
  • Prevent starting tasks with unsatisfied dependencies
  • Create a method to get the dependency chain

๐ŸŒŸ Challenge 2: Task History

Goal: Track the history of changes to each task.

  • Add a history property (array of change records)
  • Record what changed, when, and what the previous value was
  • Create a method to get the full history of a task
  • Create a method to undo the last change

๐ŸŒŸ Challenge 3: Subtasks

Goal: Add support for nested subtasks.

  • Add a subtasks property (array of Tasks)
  • Create methods to add/remove subtasks
  • Update statistics to include subtask counts
  • Calculate completion percentage based on subtasks

๐ŸŒŸ Challenge 4: Persistence

Goal: Save and load tasks from localStorage.

  • Create a save() method to persist tasks
  • Create a load() method to restore tasks
  • Handle Date serialization/deserialization
  • Add auto-save functionality

๐ŸŒŸ Challenge 5: Advanced Filtering

Goal: Add more sophisticated filtering options.

  • Date range filtering (created between X and Y)
  • Text search in title/description
  • Regex pattern matching
  • Compound filters with AND/OR logic

๐ŸŒŸ Challenge 6: Bulk Operations

Goal: Add methods for bulk operations.

  • bulkUpdate(ids: string[], updates: Partial<Task>)
  • bulkDelete(ids: string[])
  • bulkStatusChange(ids: string[], status: Status)
  • Return results with success/failure for each operation
graph TD A[Task Manager] --> B[Core Features โœ…] A --> C[Extensions ๐ŸŒŸ] B --> B1[CRUD] B --> B2[Filter & Sort] B --> B3[Statistics] C --> C1[Dependencies] C --> C2[History] C --> C3[Subtasks] C --> C4[Persistence] C --> C5[Advanced Filters] C --> C6[Bulk Operations] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style B fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff style C fill:#FFC107,stroke:#333,stroke-width:2px

๐ŸŽ“ Reflection & Next Steps

๐ŸŽ‰ Congratulations on Completing Module 1!

You've built a complete, type-safe Task Manager application from scratch. This project showcases your mastery of TypeScript fundamentals:

  • ๐Ÿ“ Designed a comprehensive type system
  • ๐Ÿ”’ Implemented type guards for runtime safety
  • โš™๏ธ Created all CRUD operations with proper typing
  • ๐Ÿ” Built advanced filtering and sorting
  • ๐Ÿ“Š Calculated statistics with Record types
  • ๐ŸŽ Used generics for flexible, reusable code

๐Ÿ’ญ Reflection Questions

Take a moment to reflect on your learning:

  • Which TypeScript feature surprised you the most?
  • What was the most challenging part of this project?
  • How do type guards improve code safety?
  • When would you use generics vs. union types?
  • How did utility types save you time?

๐Ÿ“š Review Checklist

Make sure you understand these key concepts:

  • โ˜‘๏ธ The difference between type aliases and interfaces
  • โ˜‘๏ธ How union types represent "OR" logic
  • โ˜‘๏ธ How intersection types represent "AND" logic
  • โ˜‘๏ธ When and how to use type guards
  • โ˜‘๏ธ How generics enable reusable, type-safe code
  • โ˜‘๏ธ Common utility types (Partial, Pick, Omit, Record)
  • โ˜‘๏ธ How to use conditional types
  • โ˜‘๏ธ Template literal types for string patterns

๐Ÿš€ What's Next?

With TypeScript fundamentals mastered, you're ready to dive into React! In Module 2: React Basics, you'll learn:

Coming in Module 2:

  • ๐ŸŽจ Introduction to React - Components, JSX, and the virtual DOM
  • ๐Ÿ“ฆ JSX and TSX - Typed templates with full TypeScript support
  • ๐Ÿงฉ Components and Props - Building reusable, type-safe components
  • ๐Ÿ’… Styling in React - CSS modules, styled-components, and Tailwind
  • โšก Events in React - Typed event handlers and synthetic events
  • ๐ŸŽฏ Module Project - Build a portfolio website with React + TypeScript!

๐ŸŽŠ Module 1 Complete!

You've mastered TypeScript fundamentals and built a real application. You're now ready to bring these skills into React development!

Take a break, celebrate your achievement, and get ready for React! ๐Ÿš€

graph LR A[Module 1
TypeScript Fundamentals] --> B[Module 2
React Basics] B --> C[Module 3
State & Interactivity] C --> D[Module 4
Data Fetching] D --> E[Full-Stack
React Apps] style A fill:#4CAF50,stroke:#333,stroke-width:3px,color:#fff style B fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff style C fill:#f0f0f0,stroke:#333,stroke-width:1px style D fill:#f0f0f0,stroke:#333,stroke-width:1px style E fill:#f0f0f0,stroke:#333,stroke-width:1px