Forever Functional: Decorators - new style higher order functions
Tags: functional, javascript, typescript, programming
Published: 2023-04-11 23:40:51

by [Federico Kereki](https://blog.openreplay.com/authors/federico-kereki)

In previous articles, we discussed what a [higher order function]((https://en.wikipedia.org/wiki/Higher-order_function)) (HOF) was and how we could use them to extend the functionality of our code, both [for JavaScript](https://blog.openreplay.com/forever-functional-higher-order-functions-functions-to-rule-functions/) and [for TypeScript](https://blog.openreplay.com/forever-functional-higher-order-functions-with-typescript/) programming. As a summary, a HOF is a function that takes functions as arguments, and possibly returns a function as a result. It happens that JavaScript is likely to get a particular version of these HOFs, called *decorators*, and in this article we're going to discuss what they are and how we can use them to simplify our coding.


## Decorators in JavaScript and Typescript

The idea for decorators has been around for a long time (for instance, Java has had them for years), and it seems to be an idea whose time has come. In terms of JavaScript support, the [TC39 proposal](https://github.com/tc39/proposal-decorators) is at [stage 3](https://tc39.es/process-document/) meaning that it's completely designed, and no changes will be forthcoming without implementation or usage data, so it could advance soon to stage 4, which would mean a future inclusion in the language standard. However, as of today, no browser supports the feature, as [www.caniuse.com](https://caniuse.com/?search=decorators) tells us:

![-](https://blog.openreplay.com/images/forever-functional-decorators-higher-order-functions/images/JkZrfgU.png)

With TypeScript, the issue is different; the language already [supports the proposal](https://www.typescriptlang.org/docs/handbook/decorators.html), so we can use them. Of course, when we get the official standard for decorators, there could be some changes, but let's not get ahead; we may have 100% compatibility. However, the feature is classed as "experimental" at this time, so you must add the `experimentalDecorators` [compiler option](https://www.typescriptlang.org/tsconfig#experimentalDecorators) to your `tsconfig.json` file:

```javascript
{
  "compilerOptions": {
    ...
    "experimentalDecorators": true
    ...
  }
}
```

So far, we haven't really explained what decorators are; let's fix that right now.

## What are decorators?

In object-oriented programming, a decorator is a [design pattern](https://en.wikipedia.org/wiki/Software_design_pattern) that allows us to modify the functionality of an object at run time. If you want to extend a class, you could always use inheritance, but this can get unwieldy over time if you end with multiple subclasses that just differ in the behavior of one or two methods.

A decorator lets you annotate (modify) classes and their members, most likely implementing some change to them. Applying a decorator to a method is equivalent to applying a HOF to a function to produce a new (modified) one. 

There are five kinds of decorators, which work on different class parts, allowing us to observe or modify them:

*  [class decorators](https://www.typescriptlang.org/docs/handbook/decorators.html#class-decorators) apply to the class constructor
*  [method decorators](https://www.typescriptlang.org/docs/handbook/decorators.html#method-decorators) apply to class methods
*  [accessor decorators](https://www.typescriptlang.org/docs/handbook/decorators.html#accessor-decorators) apply to accessors (setters and getters) and work similarly to method decorators
*  [property decorators](https://www.typescriptlang.org/docs/handbook/decorators.html#property-decorators) apply to class properties
*  [parameter decorators](https://www.typescriptlang.org/docs/handbook/decorators.html#parameter-decorators) apply to individual parameters for methods

Decorators can also be applied to instance or static methods, and this is done in a specific sequence; check [this reference](https://www.typescriptlang.org/docs/handbook/decorators.html#decorator-evaluation) for more on that.

In this article, we will work with method decorators, which most closely match the HOF examples we saw in previous articles. Still, the code we'll see would similarly apply to class and accessor decorators. This can sound mysterious, so let's jump into a few examples. We'll work with the following simple class:

```typescript
class Person {
  name: string = "";
  birthYear: number = 0;

  constructor(name: string, birthYear: number) {
    this.name = name;
    this.birthYear = birthYear;
  }

  asString() {
    return `I'm ${this.name}, ${this.age(new Date().getFullYear())} year(s) old.`
  }

  age(currentYear: number) {
    return currentYear - this.birthYear; // not very precise...
  }
}
```

We are defining a `Person` class with two attributes (the person's name and birth year) and a couple of methods to return a string describing the person and to (approximately...) calculate his age in some year. Let's try the `asString()` method, which we'll use in other examples.

```typescript
const myself = new Person("John Doe", 2001)
console.log(myself.asString());
// I'm John Doe, 22 year(s) old.
```

Our code works, so let's start decorating its methods.

## A logging decorator

In our [previous article](https://blog.openreplay.com/forever-functional-higher-order-functions-functions-to-rule-functions/), we wrote a logging HOF in JavaScript, which added logging when the function was called and when it returned, either normally or by throwing an error.

```javascript
const addLogging =
  (fn) =>
  (...args) => {
    console.log("Enter", fn.name, ...args);
    try {
      const toReturn = fn(...args);
      console.log("Exit ", fn.name, toReturn);
      return toReturn;
    } catch (err) {
      console.log("Error", fn.name, err);
      throw err;
    }
  };
```

Let's turn this code into a `@withLogging()` decorator. A decorator is a function that receives three arguments: the `target` object with which it will work, the `propertyKey` that names the decorated method, and a `descriptor` with more information about the property. In our case, we want to replace the original method with a new one that will include logging, as shown above. 

```typescript
function addLogging(
  target: Object,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;                 // (1)
  descriptor.value = function (...args: any[]) {           // (2)
    console.log("Enter", originalMethod.name, ...args);    // (3)
    try {
      const toReturn = originalMethod.call(this, ...args); // (4)
      console.log("Exit ", originalMethod.name, toReturn);
      return toReturn;
    } catch (err) {
      console.log("Error", originalMethod.name, err);
      throw err;
    }
  };
}
```

We get the original method by using `descriptor.value` in (1), and we'll substitute a version of our own in (2). Instead of logging `fn.name`, we use `originalMethod.name` in (3). How we call the method also changes: instead of `fn(...args)` we must use [`.call()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call) in (4) to provide the correct context; we could have used [`.apply()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply) instead.

Let's apply this decorator to our `asString()` and `age()` methods.

```typescript
class Person {
  .
  .
  .
  @addLogging
  asString(): string { ... }

  @addLogging
  age(currentYear: number): number { ... }
}
```

Note that we don't add parentheses to the decorator call; it's just a `@` character followed by the decorator's name. With the decorator added, our methods now do logging:

```typescript
const myself = new Person("John Doe", 2001);
console.log(myself.asString());
// Enter asString
// Enter age 2023
// Exit  age 22
// Exit  asString I'm John Doe, 22 year(s) old.
// I'm John Doe, 22 year(s) old.
```

Great! Logging works; let's also have a second decorator, which we'll use for timing.



<h2>Session Replay for Developers</h2> 
<p><em>
Uncover frustrations, understand bugs and fix slowdowns like never before with <strong><a href="https://github.com/openreplay/openreplay" target="_blank">OpenReplay</a></strong> — an open-source session replay suite for developers. It can be <strong>self-hosted</strong> in minutes, giving you complete control over your customer data
</em></p>
<img alt="OpenReplay" width="768" height="400" src="https://blog.openreplay.com/assets/overview_Z17Qdyg.png" class="astro-UXNKDZ4E" loading="lazy" decoding="async">
<em>Happy debugging! <a href="https://openreplay.com" target="_blank">Try using OpenReplay today.</a></em>



## A timing decorator

Let's go back to our timing HOF from the [earlier article](https://blog.openreplay.com/forever-functional-higher-order-functions-functions-to-rule-functions/):

```typescript
const addTiming = (fn) => (...args) => {
  let start = performance.now();  
  try {
    const toReturn = fn(...args); 
    console.log("Normal exit", fn.name, performance.now()-start, "ms");
    return toReturn;
  } catch (thrownError) {         
    console.log("Exception thrown", fn.name, performance.now()-start, "ms");
    throw thrownError;
  }
};
```

We can apply a similar transformation to produce an `@addTiming` decorator.

```typescript
function addTiming(
  target: Object,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    let start = performance.now();
    try {
      const toReturn = originalMethod.apply(this, args);
      console.log("Normal exit", originalMethod.name, performance.now() - start, "ms");
      return toReturn;
    } catch (err) {
      console.log("Exception thrown", originalMethod.name, performance.now() - start, "ms");
      throw err;
    }
  };
}
```

Using this decorator is equally straightforward.

```typescript
class Person {
  .
  .
  .
  @addTiming
  asString(): string { ... }

  @addTiming
  age(currentYear: number): number { ... }
}

const myself = new Person("John Doe", 2001);
console.log(myself.asString());
// Normal exit age 0.0205250084400177 ms
// Normal exit asString 1.1684779822826385 ms
// I'm John Doe, 22 year(s) old.
```

OK, we now have a couple of valid decorators, and we could create more if needed. Let's see how we can *compose* decorators, so we'll apply several to the same method.


## Composing decorators

Composing decorators means that we apply one decorator (which modifies the method it's applied to), and we apply a second decorator to the result of the first one, then a third to the result of the second, etc. To compose decorators, just list them in order.

```typescript
class Person {
  .
  .
  .
  @addLogging
  @addTiming
  age(currentYear: number): number {
    return currentYear - this.birthYear; // not very precise...
  }
}

const myself = new Person("John Doe", 2001);
console.log(myself.asString());
// Enter  2023
// Normal exit age 0.01621299982070923 ms
// Exit   22
// I'm John Doe, 22 year(s) old.

```

In this case, we are applying `@addLogging` to the result of applying `@addTiming` to the `age()` method -- the output confirms this result. Keep in mind that the last listed decorator is applied first; it's as if working with functions we had written something like `addLogging(addTiming(age))`.

Now we know how to create decorators and also how to compose them for more flexibility. Let's finish this article by seeing how we can write *decorator factories*, that allow us to customize how decorators work.

## Decorator factories

Let's suppose we want to be able to decide when logging should occur. We could want to enable or disable each specific log: the one made at entering the function, the one when returning a value, and the one on error. We must write a [decorator factory](https://www.typescriptlang.org/docs/handbook/decorators.html#decorator-factories), a HOF by itself; it's a function that will return a decorator, so we'll have a HOF returning a HOF!

We'll modify our logging decorator to receive an optional object with three boolean flags; if this parameter is not provided, we'll log everything by default.

```typescript
function addLogging2(
  when: { onEnter?: boolean, onReturn?: boolean, onError?: boolean } = {
    onEnter: true,
    onReturn: true,
    onError: true,
  }
) {
  return function (
    target: Object,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      if (when?.onEnter) {
        console.log("Enter", originalMethod.name, ...args);
      }
      try {
        const toReturn = originalMethod.apply(this, args);
        if (when?.onReturn) {
          console.log("Exit ", originalMethod.name, toReturn);
        }
        return toReturn;
      } catch (err) {
        if (when?.onError) {
          console.log("Error", originalMethod.name, err);
        }
        throw err;
      }
    };
  };
}
```

Study our function carefully: calling `addLogging2(...)` returns a decorator itself! Let's use this factory to enable different types of logging for our `Person` class.


```typescript
class Person {
  .
  .
  .
  @addLogging2({ onEnter: true })
  asString(): string { ... }

  @addLogging2()
  age(currentYear: number): number { ... }
}
```

We want `asString()` to log only on entry, but we'll allow all logging for `age()`.  -- the output confirms that our decorators performs as expected; no "Exit" logs for `asString()`.

```typescript
const myself = new Person("John Doe", 2001);
console.log(myself.asString());
// Enter asString
// Enter age 2023
// Exit  age 22
// I'm John Doe, 22 year(s) old.
```

We've done it! 

## Conclusion

In this article, we've discussed decorators and how we can write them starting with the HOFs from previous articles. We focused on decorators for methods, but you should look into all their possibilities because they promise to let us write code in much more efficient ways. 




[![newsletter](https://blog.openreplay.com/newsletter_Z1P0UIP.png)](https://newsletter.openreplay.com/)