-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
46: Practice Exercise: Building a Generic Repository Class
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
Storablethat requires anid: string. - Create two interfaces,
MovieandUser, both extendingStorable.Movieshould have atitle: string, andUsershould have ausername: string. - Implement a generic class
DataStore<T extends Storable>. - The
DataStoreclass must include:- A private array to hold the items.
- An
addItem(item: T): voidmethod. - A
removeItem(id: string): voidmethod that filters out the item with the matching ID. - A
findItem(id: string): T | undefinedmethod.
- Instantiate a
movieStoreand auserStore, and verify that you cannot add a plain object (like{ name: "Test" }) that doesn't satisfy theStorableinterface.
There are no comments for now.