Skip to Content
Course content

243: Typing a Cache Layer with Generics

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

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 returns T | undefined.
  • Implement a clear() method to empty the cache.
  • Define an interface DashboardStats with visitors: number and revenue: number.
  • Instantiate a ResponseCache specifically for DashboardStats.
  • Attempt to set a value that does not match the DashboardStats interface and verify that TypeScript flags it as an error.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.