๐ฏ 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 |
๐จ Interactive: Task Data Structure
Explore the Task interface structure. Click on each property to see its type and purpose:
โ 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!
๐จ 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:
๐งช 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
dependsOnproperty (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
historyproperty (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
subtasksproperty (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
๐ 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! ๐
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