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
243: Typing a Cache Layer with Generics
I've seen this pattern in dozens of code reviews. A developer needs a simple way to store data in memory to avoid hitting a database too hard, so they build a "generic" cache. But usually, when they say "generic," they actually mean they used any.
type Cache = Record<string, any>;
class SimpleCache {
private store: Cache = {};
set(key: string, value: any) {
this.store[key] = value;
}
get(key: string) {
return this.store[key];
}
}
// Usage
const userCache = new SimpleCache();
userCache.set('user_1', { id: 1, name: 'Alice' });
const user = userCache.get('user_1');
// TypeScript thinks 'user' is 'any', so this doesn't error...
// ...until it crashes at runtime because I misspelled 'name'
console.log(user.namme.toUpperCase());
The "any" Hole in Your Type Safety
The problem here is that our SimpleCache is too flexible. By using any, we've essentially told TypeScript to stop helping us. The moment you call get(), you lose all knowledge of what that data actually is. In a large project, this is a nightmare. You might accidentally store a Product object in a cache you thought was only for User objects, and you won't find out until a user hits a 500 error in production.
We want a cache that is reusable—meaning it can handle users, products, or configuration settings—but once we instantiate a specific cache, it should be locked to a specific type.
Binding the Cache to a Specific Type
This is exactly where generics shine. Instead of defining the value as any, we use a type parameter (usually T) that gets defined when the class is created. I like to think of this as creating a "template" for the class.
class TypedCache<T> {
// Now our store maps strings to whatever T is
private store: Record<string, T> = {};
set(key: string, value: T): void {
this.store[key] = value;
}
get(key: string): T | undefined {
return this.store[key];
}
}
interface User {
id: number;
name: string;
}
// We tell the cache: "For this instance, T is a User"
const userCache = new TypedCache<User>();
userCache.set('user_1', { id: 1, name: 'Alice' });
const user = userCache.get('user_1');
if (user) {
// Now TypeScript knows 'user' is a User object.
// This will now throw a compile-time error!
console.log(user.namme.toUpperCase());
}
By shifting the type definition from the class implementation to the class instantiation, we've reclaimed our type safety. The TypedCache class doesn't need to know what a User is; it just knows that whatever T is, the set method must receive it, and the get method will return it.
Handling Dynamic Keys
Sometimes, a string key isn't enough. Maybe you're caching data based on a numeric ID or a specific enum. We can make the cache even more flexible by adding a second generic parameter for the key.
class FlexibleCache<K, V> {
private store: Map<K, V> = new Map();
set(key: K, value: V): void {
this.store.set(key, value);
}
get(key: K): V | undefined {
return this.store.get(key);
}
}
// A cache where keys are numbers (ID) and values are User objects
const userByIdCache = new FlexibleCache<number, User>();
userByIdCache.set(101, { id: 101, name: 'Bob' });
I prefer using Map over a plain object (Record) when dealing with generic keys. Objects in JavaScript essentially force keys to be strings or symbols, whereas Map actually respects the type of the key you provide. If you're building a professional-grade cache layer, Map is almost always the right choice.
📋 Practical Task
Exercise: Implementing a Type-Safe API Response Cache
You are building a data-fetching layer for a dashboard. You need to create a ResponseCache class that stores API responses. To ensure the dashboard doesn't crash, the cache must be strictly typed so that the developer knows exactly what kind of data is being retrieved for a specific endpoint.
Requirements:
- Create a class
ResponseCache<T>. - Implement a
set(key: string, value: T)method. - Implement a
get(key: string)method that returnsT | undefined. - Implement a
clear()method to empty the cache. - Define an interface
DashboardStatswithvisitors: numberandrevenue: number. - Instantiate a
ResponseCachespecifically forDashboardStats. - Attempt to
seta value that does not match theDashboardStatsinterface and verify that TypeScript flags it as an error.
There are no comments for now.