We frequently build caches to improve actual and perceived application performance for our end users. This article describes how a TS language feature, generics, can help us build a cache.

The first thing you need to do when building a cache (and really any kind of generic service utility) is to define its public interface. At its core, a typical cache has some way to store and retrieve values. A more fully functional cache will also let you inspect the cache, clear one or more items from the cache and other goodness, but for he sake of this article, we’ll say that we just have two things to do: Put and Get.

Let’s consider Put. When we put an item into the cache, we often want to assign some a lifetime for that item. This is the window in time that the item is valid*. We are eventually going to retrieve items from the cache and in order to do that, we need some way to uniquely identify that cached item. We’ll call that unique identity the item’s key. Put it all together and we have a workable function signature for Put:

public Put(key: string, data: any, lifetimeInMilliseconds: number): void

Things are bit more interesting with Get. As you can, when we put the data into the cache, it’s an “any” time. This is fine since the cache is indifferent to the type of data it’s caching (at least in the abstract). If we took it no further than this, we’d have a Get signature like this:

public Get(key: string): any

We can do better in TypeScript using generics. We can instead decorate Get() with a generic type and tell it the actual data type we expect to retrieve. It looks like this:

public Get<T>(key:string) : T

This is telling the TypeScript compiler that we’re going to issue a Get against the cache. However, we’re also going to tell it that we already know/expect the data type of the response: “T”.

Imagine if we have cached two objects, one named “UserProfile” and the othe named “FavoriteItem”. With TypeScript generics, I do this:

const cachedProfile = cacheService.Get<UserProfile>(“someKey”);

const cachedFaves = cacheService.Get<FavoriteItem[]>(“someOhterKey”);

Since we’re now telling Get that we expect a UserProfile in the first case and an array of FavoriteItem in the other, the corresponding const vars are now strongly typed.

This is a very basic example and generics can go a lot further. Consider, for example, that JS objects are actually complicated things to work with generically. In the real cache utility I wrote, my cacheable objects are simple — they just hold data. They don’t have their own functions or contain other nested objects. Hence, I can safely serialize and deserialize them using JSON.stringify() and JSON.parse(). This isn’t good enough for more complex objects. Using generics, we could tell TypeScript that we are only allowed to work with <T>’s that implement a smart Serialize() and Deserialize() method. The code would look like this:

public Put<T extends Serializable>(key: string, data: T, lifetime: number);

public Get<T extends Serializable>(key: string): T;

At runtime, the TS compiler won’t let us use this particular cache service on objects unless they implement an interface named “Serializable.”

If you’d like to learn more about this, check out this previous article I wrote: https://medium.com/@pagalvin/implement-binary-search-in-typescript-using-generics-with-useful-refactorings-a4bcda932d7#.tv0of7jdo.

Generics give us a great tool for telling the TypeScript compiler what we expect the data to look like at runtime. This, in turns, gives it the information it needs in order to provide great intellisense and other dev-time goodness to help us be more productive.

(*) Using time to live is just one of many options. This quote is apt:

There are only two hard things in Computer Science: cache invalidation and naming things. — Phil Karlton

</end>