Skip to Content
Course content

46: Practice Exercise: Building a Generic Repository Class

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Alright, let's get our hands dirty. Imagine we're building a small backend for a movie rental system. We have Movie objects and Customer objects. Both need basic CRUD operations—saving to a database, finding by ID, and listing all records. My first instinct, if I'm rushing, is to just write two different classes. Let's see how that looks.

The "Copy-Paste" Trap

interface Movie { id: string; title: string; genre: string; }
interface Customer { id: string; name: string; email: string; }

class MovieRepository {
  private movies: Movie[] = [];
  add(movie: Movie) { this.movies.push(movie); }
  getById(id: string) { return this.movies.find(m => m.id === id); }
}

class CustomerRepository {
  private customers: Customer[] = [];
  add(customer: Customer) { this.customers.push(customer); }
  getById(id: string) { return this.customers.find(c => c.id === id); }
}

I mean, it works. But look at those two classes. They are virtually identical. If I decide tomorrow that getById should throw a custom EntityNotFoundError instead of returning undefined, I have to go into every single repository class in my codebase and change it. That's a maintenance nightmare waiting to happen. I need a way to define the logic once, but keep the types specific.

The Temptation of any

I could try to make a single class that handles everything by using any. Let's try that and see where it breaks.

class GenericRepository {
  private items: any[] = [];
  add(item: any) { this.items.push(item); }
  getById(id: string) { return this.items.find(i => i.id === id); }
}

At first glance, this is great. I can use it for movies, customers, whatever. But here's the problem: the moment I call repo.getById('123'), TypeScript has no idea what comes back. It's just any. I lose all the autocomplete and type safety that makes TypeScript worth using in the first place. If I try to access .title on a result that turns out to be a Customer, the compiler won't warn me, and I'll get a runtime error. Not acceptable.

Introducing the Type Variable

This is where generics actually save us. Instead of telling the class exactly what it's holding, or telling it "I don't care" with any, I can use a placeholder. I'll call it T (the industry standard for "Type").

class Repository<T> {
  private items: T[] = [];
  add(item: T) { this.items.push(item); }
  getAll(): T[] { return this.items; }
}

Now, when I instantiate it, I tell it which type to use: const movieRepo = new Repository<Movie>(). Now add() expects a Movie and getAll() returns a Movie[]. We've got our type safety back. But wait—there's a snag.

The "Where is the ID?" Problem

I want to bring back the getById method. If I try to add it to my generic class like this:

getById(id: string) {
  return this.items.find(item => item.id === id);
}

TypeScript will scream at me. It'll say Property 'id' does not exist on type 'T'. This makes sense. T could be anything—a string, a number, or an object that doesn't have an id. The compiler can't guarantee that whatever T ends up being, it will have an id property.

To fix this, I need to constrain T. I'll create a base interface that defines what it means to be an "entity" in my system, and then tell the repository that T must extend that interface.

interface Entity {
  id: string;
}

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

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

  getById(id: string): T | undefined {
    // Now TypeScript knows for sure that item has an id!
    return this.items.find(item => item.id === id);
  }
}

Now it's airtight. If I try to create a Repository<string>(), TypeScript will stop me because a string doesn't have an id property. But Movie and Customer (assuming they both have an id) work perfectly. We've successfully abstracted the behavior without sacrificing the types.




📋 Practical Task

Exercise: Implementing a Generic Movie Rental Data Store

You are tasked with building a type-safe data management layer for a movie rental app. Follow these requirements:

  • Create an interface called Storable that requires an id: string.
  • Create two interfaces, Movie and User, both extending Storable. Movie should have a title: string, and User should have a username: string.
  • Implement a generic class DataStore<T extends Storable>.
  • The DataStore class must include:
    • A private array to hold the items.
    • An addItem(item: T): void method.
    • A removeItem(id: string): void method that filters out the item with the matching ID.
    • A findItem(id: string): T | undefined method.
  • Instantiate a movieStore and a userStore, and verify that you cannot add a plain object (like { name: "Test" }) that doesn't satisfy the Storable interface.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.