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
41: Generics in Classes and Functions
I've seen this exact pattern in a dozen different code reviews. A developer wants to build a utility—something like a cache, a data store, or a wrapper—that can handle "anything." They start with any because it's the path of least resistance. It works for five minutes, and then it blows up in production.
The 'any' Trap in Data Wrappers
class DataStore {
private data: any;
set(value: any): void {
this.data = value;
}
get(): any {
return this.data;
}
}
const userStore = new DataStore();
userStore.set({ id: 1, name: "Alice" });
// Later in the code, someone forgets what was stored...
const user = userStore.get();
console.log(user.toUpperCase()); // Runtime Error: user.toUpperCase is not a function
The problem here isn't that the code doesn't run; it's that TypeScript has stopped helping you. By using any, you've essentially turned off the compiler. The get() method tells TypeScript, "I'm returning something, but I have no idea what it is," so TypeScript lets you call toUpperCase() on a user object. You won't catch this until the app crashes in a user's browser.
Locking the Type with Generic Classes
We want a class that is flexible enough to hold any type, but strict enough to remember which type it's currently holding. That's where Generics come in. Instead of any, we use a type variable (usually T).
class DataStore<T> {
private data: T;
set(value: T): void {
this.data = value;
}
get(): T {
return this.data;
}
}
// Now we tell the class exactly what T is for this instance
const userStore = new DataStore<{ id: number, name: string }>();
userStore.set({ id: 1, name: "Alice" });
const user = userStore.get();
// console.log(user.toUpperCase()); // COMPILER ERROR: Property 'toUpperCase' does not exist on type...
console.log(user.name.toUpperCase()); // This works!
By adding <T> to the class definition, we've created a blueprint. When we instantiate new DataStore<User>(), TypeScript replaces every instance of T inside that class with User for that specific object. It's like a search-and-replace that happens at compile time.
Passing Types through Functions
Classes aren't the only place this is useful. You'll often write "utility" functions that don't care about the specific data they're handling, only the shape of the operation. Take this common scenario: you need a function that wraps a value in an array, but you want the resulting array to keep the type of the original value.
function wrapInArray<T>(value: T): T[] {
return [value];
}
const numbers = wrapInArray(10); // TypeScript infers T is number, returns number[]
const strings = wrapInArray("Hello"); // TypeScript infers T is string, returns string[]
Notice I didn't have to write wrapInArray<number>(10). TypeScript is smart enough to look at the argument you passed in and "infer" what T should be. I always recommend letting inference do the work unless the compiler gets confused or you need to be explicit for readability.
📋 Practical Task
Build a Type-Safe API Response Wrapper
In real-world apps, API responses usually follow a standard envelope (e.g., always containing a status and a data payload). Your task is to create a generic class that handles this envelope regardless of what the data actually is.
Requirements:
- Create a class called
ApiResponse<T>. - The class should have two properties:
status: numberandpayload: T. - Add a method called
logPayload()that prints the payload to the console. - Instantiate the class twice:
- Once for a
Usertype (withusernameandemail). - Once for a
Producttype (withtitleandprice).
- Once for a
Make sure that if you try to access a price on the User response, TypeScript throws a compile-time error.
There are no comments for now.