TypeScript
Completed
-
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
151: Generic Classes
I've noticed a recurring pattern when developers first move from generic functions to generic classes. There's this nagging feeling that class Box<T> is basically just a fancy way of saying "this class uses any, but I'm promising TypeScript I'll be careful."
Generics are not just "type-safe any"
If you treat a generic class like a container for any, you're missing the entire point of the feature. The misconception is thinking that T remains fluid throughout the life of the object. It doesn't. Once you instantiate a generic class, T is locked in for that specific instance.
// The "I think this is just any" approach
class DataStore<T> {
private storage: T[] = [];
add(item: T) { this.storage.push(item); }
getLatest(): T { return this.storage[this.storage.length - 1]; }
}
// Someone might think they can do this:
const myStore = new DataStore(<any>());
myStore.add("Hello");
myStore.add(42); // This works because they used 'any', but it's useless.
The power of generic classes isn't about allowing any type—it's about enforcing consistency across the entire instance. When you instantiate new DataStore<string>(), you aren't just telling TypeScript what's inside; you're creating a contract that the add method and the getLatest method must agree on the same type for the rest of that object's life.
Locking the type at instantiation
Let's look at a real-world scenario: a ResponseWrapper. In a professional API client, you don't want to manually cast your data every time you get a response. You want the class to "remember" what it's holding.
interface User { id: number; name: string; }
interface Product { id: number; price: number; }
class ResponseWrapper<T> {
constructor(public data: T, public timestamp: Date) {}
logData() {
console.log(`Received data at ${this.timestamp}:`, this.data);
}
}
// Now, these are two completely different types of objects
const userResponse = new ResponseWrapper<User>({ id: 1, name: "Alice" }, new Date());
const productResponse = new ResponseWrapper<Product>({ id: 101, price: 29.99 }, new Date());
// TypeScript knows exactly what's inside.
// No casting needed, and no 'any' in sight.
console.log(userResponse.data.name); // Safe!
console.log(productResponse.data.price); // Safe!
I love this pattern because it removes the "guesswork" from your data layer. The class becomes a blueprint, and the instantiation becomes the specific implementation.
Adding guardrails with the extends keyword
Sometimes, leaving T completely open is too permissive. You might want a generic class, but you need to ensure that whatever T is, it has certain properties. This is where constraints come in. If you try to access a property on T without a constraint, TypeScript will scream at you because it can't guarantee that T will actually have that property.
interface Identifiable {
id: string;
}
// We tell TS: "T can be anything, as long as it has at least an id string"
class Repository<T extends Identifiable> {
private items: Map<string, T> = new Map();
save(item: T) {
this.items.set(item.id, item); // This only works because of 'extends Identifiable'
}
findById(id: string): T | undefined {
return this.items.get(id);
}
}
// This works because User has an id
interface User { id: string; username: string; }
const userRepo = new Repository<User>();
// This will fail! Product is missing the 'id' string.
interface Product { sku: string; price: number; }
// const productRepo = new Repository<Product>();
By using extends, you're essentially saying: "I want the flexibility of generics, but I need a minimum guarantee of what the data looks like." It's the best of both worlds.
📋 Practical Task
Build a Type-Safe PaginatedResult Container
You need to create a generic class called PaginatedResult<T> that handles a list of items and metadata about the pagination. This is a common requirement for backend-to-frontend data transfer.
Requirements:
- The class should accept a generic type
T. - It should have a constructor that takes:
items: An array ofT.totalPages: A number.currentPage: A number.
- Implement a method
hasNextPage(): booleanthat returns true if thecurrentPageis less than thetotalPages. - Implement a method
getFirstItem(): T | nullthat returns the first item in the list or null if the list is empty.
Test your implementation: Create a Customer interface with name and email. Instantiate your PaginatedResult using this interface and verify that getFirstItem() correctly returns a Customer object with full type-hinting (autocomplete for name and email).
There are no comments for now.