published on

Build a Simple in Memory Cache With Typescript Decorators

Hello everyone! Many years ago when I started programming, the first language I fell in love with was Python. With its simple yet powerful language features ranging from comprehensions to generators, the language enabled me to solve complex problems efficiently.

In the past few years, my daily work started involving less coding with Python and more with TypeScript. Frankly, moving from a type-less system to a typed one made my life a whole lot easier, however, I started missing some of the great flavours that Python gave me out of the box. One of these tools were decorators, and I learned that TypeScript has (some) support for them! So in this post, we will be taking a look at a practical introduction to TypeScript decorators, their advantages, and their shortcomings. Let’s start with the basics.

What is a decorator?

According to Wikipedia, a decorator is a software pattern that enables developers to enhance the capabilities of an object without modifying the concrete object itself. There are many types of decorators in the wild, ranging from class decorators, object decorators, function decorators and so on. Today we will be taking a look at method decorators.

Let’s kick off with a simple algorithm, calculating the nth member of the Fibonacci sequence.

class MathExtensions {
	fib(n: number): number { 
    if (n === 0 || n === 1) {
      return 1;
    }
    return this.fib(n - 1) + this.fib(n - 2);
  }
}

const me = new MathExtensions();
console.log(me.fib(10));

This simple module above calculates the 10th member of the Fibonacci sequence for us. Most of you by this time know that the above algorithm doesn’t deal well with high values of n, so for this we usually use a technique called memoization to store the previous values of the sequence to avoid a stack overflow.

class MathExtensions {
  memo: { [k: string]: number } = {};
  fib(n: number): number { 
    if (n === 0 || n === 1) {
      return 1;
    }
    if (!this.memo.hasOwnProperty(n)) {
      this.memo[n] = this.fib(n - 1) + this.fib(n - 2);
    }
    return this.memo[n];
  }
}

Nice, it’s nice and fast now and doesn’t crash for high(er) numbers of n. This is such a nice pattern though. Storing the return values of functions in memory to save computation power can be used in many places, how can we make this generic? You guessed it, decorators are a potential way of solving this! Let’s take a look at how they work in TypeScript.

Decorators in TypeScript

Now that we are familiar with the problem space, it’s time to dive a bit deeper. In TypeScript we can easily define a new decorator the following way:

function decorator() {
  // Code here runs when your decorator initializes
	return function (target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    // Do something here with your decorator
  }
}

The above function is a decorator factory, it returns a new function with the target. propertyKey and descriptor parameters as inputs.

target - The name of the class this decorator was used in. In our example, it will be MathExtensions. propertyKey - The name of the property of the class the decorator was applied to. In our case, this can be fib. descriptor - An object that describes our property that we applied the decorator on. It has the following properties: * value - The value that is assigned to the property. In our case, it will be the actual function fib. * configurable - Indicates if the property descriptor can be modified in the future (you can read more about this on MDN) * enumerable - Indicates if the property can be accessed via the in operator. * writable - Indicates if the property can be modified or not.

In this tutorial we will mostly be fidgeting with descriptor.value. Let’s take a look at a very basic decorator.

My first TypeScript decorator

Alright, let’s start with the basics. In this section we will create a decorator that logs something when a function was called.

function log(prefix: string) {
  return function (target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log(`${prefix} - ${propertyKey}`);
  }
}

The above decorator takes an initialisation parameter that we are going to print out every time the function it’s applied to is called. We will also log the name of the function for clarity. Applying the decorator is simple, you can just add the @ prefix when calling it above the method you’d like it to be applied to:

class MathExtensions {
  @log("running method")
	fib(n: number): number { 
    if (n === 0 || n === 1) {
      return 1;
    }
    return this.fib(n - 1) + this.fib(n - 2);
  }
}

const me = new MathExtensions();
console.log(me.fib(10));

The above code should have the following output:

[LOG]: "running method - fib" 
[LOG]: 89 

Looks simple, but already can be super powerful. Maybe you’d like to measure the usage of some methods on a module that you’ve created, the above decorator will give you a reusable way to do so. Feel free to modify the decorator any way you want. As an exercise, you can implement a timestamp to prefix the logs.

Now that we have written our first decorator of this journey, let’s take a look at the problem that we were trying to solve originally. Creating a reusable memoization system.

The memo decorator

The memo decorator is slightly different from what we have seen above, because we would like to change the behaviour of our methods that we are applying them to. This is where the PropertyDescriptor comes into play, particularly the value parameter. Since for methods, the value parameter contains the entire method that we are applying the decorator to, we can take it and modify it to add some extra functionality. Here’s the code:

function memo() {
  const cache: { [k: string]: any } = {};
  return function (target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const cacheKey = `__cacheKey__${args.toString()}`;
      if (!cache.hasOwnProperty(cacheKey)) {
        cache[cacheKey] = originalMethod.apply(this, args);
      }
      return cache[cacheKey];
    }
  }
}

That’s a lot of code, so let’s go through it almost line by line. Right after we define the memo function, in the body we create a variable called cache. This variable will the our storage where we will be saving the results of our method calls.

Inside the return value of memo we store the original value of the descriptor. This will ensure us that we didn’t modify the actual method itself.

In the next line, we are updating the value of the descriptor with the logic that we want. In this case, we are going to create a cache key from the method arguments, check if that cache key exists in our cache and if not, we evaluate the method and store the results.

Note here: This implementation is, of course, imperfect. Since the toString method can return [object Object] or some other weirdness for complex data types, we would need a better way to calculate a cache key here.

So, with all that out of the way, here’s the final piece of code that we are shipping:

function log(prefix: string) {
  return function (target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log(`${prefix} - ${propertyKey}`);
  }
}

function memo() {
  const cache: { [k: string]: any } = {};
  return function (target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const cacheKey = `__cacheKey__${args.toString()}`;
      if (!cache.hasOwnProperty(cacheKey)) {
        cache[cacheKey] = originalMethod.apply(this, args);
      }
      return cache[cacheKey];
    }
  }
}

class MathExtensions {
  @log("running method")
  @memo()
  fib(n: number): number { 
    if (n === 0 || n === 1) {
      return 1;
    }
    return this.fib(n - 1) + this.fib(n - 2);
  }
}

const me = new MathExtensions();
console.log(me.fib(1000));

As you can see above, we were able to use multiple decorators on the same function. And now we have a very clean Fibonacci solution that doesn’t crash for large values of n. Feel free to copy the code above to the TypeScript Playground and try your fingers on it!

Thanks for sticking with me for this short lesson on decorators. Hope you’ve learned something new, looking forward to our next session!