Thuta Learning
ProjectsProgrammingintermediate

Mini Project: Task Manager - Part 3 (Generics & Polish)

Relax. We'll talk through this in plain words — no textbook voice.

What you'll walk away with

  • Apply Mini Project: Task Manager - Part 3 (Generics & Polish) in a real project
  • Write and run the code yourself
  • Build out an entire project step by step

Let's think about this for a second

In this final stage of the project, we'll use generics to refactor the code into something more flexible. Instead of a TaskManager that only works with the Task type, we'll build a generic Repository<T> class that can store any type. This shows how real-world projects can cut down on code duplication. We'll also add a function that uses a tuple type to return summary statistics (total count, done count). Once this stage is done, the whole project will be complete across three layers: setup, features, and polish.

Let's build it

Create a generic class called Repository<T> and implement the methods private items: T[] = [], add(item: T): void, getAll(): T[], and find(predicate: (item: T) => boolean): T | undefined. Rewrite the TaskManager class to use Repository<Task> internally (as composition). Add a function getStats(): [number, number] that returns a tuple in the form [total, doneCount]. Finally, add error handling so that when updateStatus() is called with an id that isn't found, it prints an error message with console.error(), then run the project as a demo.

Sample code

typescript
class Repository<T> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  getAll(): T[] {
    return this.items;
  }

  find(predicate: (item: T) => boolean): T | undefined {
    return this.items.find(predicate);
  }
}

function getStats(repo: Repository<Task>): [number, number] {
  const all = repo.getAll();
  const total = all.length;
  const doneCount = all.filter((t) => t.status === TaskStatus.Done).length;
  return [total, doneCount];
}

const taskRepo = new Repository<Task>();
taskRepo.add({ id: 1, title: "Learn generics", status: TaskStatus.Done, priority: "high" });
taskRepo.add({ id: 2, title: "Write tests", status: TaskStatus.Todo, priority: "medium" });

const [total, done] = getStats(taskRepo);
console.log(`Total: ${total}, Done: ${done}`);

const missing = taskRepo.find((t) => t.id === 99);
if (!missing) {
  console.error("Task not found: id 99");
}
You should see
The console shows "Total: 2, Done: 1", plus an error message since id 99 can't be found.

5-minute try it yourself

Try reusing the Repository<T> class for a string[] case too (e.g. a tags list), and confirm that a single generic class can serve two cases with different data types.

A quick word of caution

In larger projects, keep in mind that using a generic class through composition (adding it as a property inside another class) tends to be more flexible and easier to test than using inheritance.

Easy traps

  • Instantiating Repository<T> without the type argument (new Repository<Task>()), letting it get inferred as any
  • Writing the tuple return type [number, number] as a plain array literal in the function's return statement and mixing up the element order (swapping total and doneCount)

Now try it yourself

Try reusing the Repository<T> class for a string[] case too (e.g. a tags list), and confirm that a single generic class can serve two cases with different data types.

You'll know it worked when: The console shows "Total: 2, Done: 1", plus an error message since id 99 can't be found.

Mini Project: Task Manager - Part 3 (Generics & Polish) | Thuta Learning