Skip to Content
Course content

82: Building a Typed In-Memory Cache with Generics

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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> where T represents the session data.
  • Implement a saveSession(token: string, data: T): void method.
  • Implement a getSession(token: string): T | null method.
  • Implement a clearSession(token: string): void method.

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.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.