-
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
82: Building a Typed In-Memory Cache with Generics
A few years ago, I was reviewing a PR for a teammate who was building a high-traffic dashboard. To save on API costs, he'd implemented a quick-and-dirty in-memory cache using a plain JavaScript object. It looked fine at first glance, but because he used any for the values, he eventually tried to call .toLowerCase() on a cached item that he *thought* was a username string, but was actually a User object. The app crashed in production with a "TypeError: ... is not a function." It was a classic case of the cache becoming a "black hole" where type safety went to die.
The fix isn't to avoid caching, but to make the cache itself type-aware. If you're building a utility that can store anything, you shouldn't be reaching for any; you should be using Generics. By parameterizing the key and the value, we can ensure that a cache created for User objects can never accidentally store a Product object.
Defining the Generic Cache Interface
When we build a cache, we generally care about two things: what the lookup key is (usually a string or number) and what the stored value is. By using <K, V>, we tell TypeScript, "I don't know what these types are yet, but once the user instantiates this class, they stay consistent."
type CacheEntry<V> = {
value: V;
expiry: number;
};
class MemoryCache<K, V> {
private storage = new Map<K, CacheEntry<V>>();
set(key: K, value: V, ttlInMs: number): void {
const expiry = Date.now() + ttlInMs;
this.storage.set(key, { value, expiry });
}
get(key: K): V | undefined {
const entry = this.storage.get(key);
if (!entry) return undefined;
if (Date.now() > entry.expiry) {
this.storage.delete(key);
return undefined;
}
return entry.value;
}
}
I prefer using a Map over a plain object here because Maps handle non-string keys much more gracefully and offer better performance for frequent additions and removals. Notice how storage is typed as Map<K, CacheEntry<V>>. This creates a strict link: the key you use to set the value must be the same type you use to get it.
Enforcing Type Boundaries in Practice
The real magic happens when you actually instantiate the cache. Because we used Generics, TypeScript will now protect you from the exact mistake my teammate made. You can create separate caches for different data models, and they will be completely isolated from one another.
interface User { id: string; name: string; }
interface Config { theme: 'light' | 'dark'; version: number; }
// This cache only accepts strings as keys and User objects as values
const userCache = new MemoryCache<string, User>();
const configCache = new MemoryCache<string, Config>();
userCache.set('user_1', { id: '1', name: 'Alice' }, 60000);
// TypeScript Error: Argument of type 'Config' is not assignable to parameter of type 'User'.
userCache.set('config_1', { theme: 'dark', version: 1 }, 60000);
const user = userCache.get('user_1');
// 'user' is automatically inferred as User | undefined. No casting needed!
If you try to pass a Config object into the userCache, the compiler will scream at you immediately. You get the flexibility of a single cache class that works for any data type, but the rigor of a hard-coded type for every specific instance. It's the best of both worlds: reusable code that doesn't sacrifice safety.
📋 Practical Task
Exercise: Building a Typed User Session Store
You need to build a session management system for a web app. Create a class called SessionStore using Generics. The store should map a string session token to a generic SessionData object (which contains the user's preferences and permissions).
- Define a generic class
SessionStore<T>whereTrepresents the session data. - Implement a
saveSession(token: string, data: T): voidmethod. - Implement a
getSession(token: string): T | nullmethod. - Implement a
clearSession(token: string): voidmethod.
To test your implementation, create an interface called UserSession with properties userId: string and role: 'admin' | 'user'. Instantiate a SessionStore<UserSession> and verify that trying to save a plain string or a number as the session data triggers a TypeScript compiler error.
There are no comments for now.