The case for using decorators in your codebase
Published: 2024-01-10 11:51:43

A **decorator** is a function that allow us to customize an existing method. It allows for the modification of an object’s behavior — without changing its original code, but extending its functionality.

Decorators are commonly used in some JS frameworks/libraries like Angular and MobX, however they are still not a common design pattern developers used while writing their own code. With this article, I aim to document how we used decorators in my experience and hope to give you some ideas to utilise this design pattern in your own codebase.

Decorators are typically used with classes and prefixed with the @ symbol:

## First, let's start with an Example

```javascript
//value = the function being decorated
//kind = type of property, would be "method" here
//name = name of the function
function logged(value, { kind, name }) {
    return function (...args) {
      console.log(`starting ${name} with arguments ${args.join(", ")}`);
      return value.call(this, ...args);
    };
}

class Example {
  @logged
  method(arg) {
    //Your method's code here
    console.log("Inside method")
  }
}
let ex = new Example()
ex.method(1);

/**
Outputs the following
[LOG]: "starting method with arguments 1" 
[LOG]: "Inside method" 
*/

```
The logged method here logs all the arguments being passed to a function. These decorators came in handy when we worked on a SDK, where the best way to debug was customer uploaded logs. Adding these decorators ensured code readability and reuse while giving our team an easy way to keep track of all the methods called by our users without much overhead. You can try running this code [here](https://www.typescriptlang.org/play?noImplicitAny=false&noImplicitThis=false&noImplicitReturns=false&experimentalDecorators=false#code/PQKhCgAIUg5B7ALgUwFyQOrIOQDdmQB2SkAhgCbnLlmECekidADsgM6QAWyATgYvEgBrZMmaNukAMbwqZDmwCWAW2YAbAqQ7N4bJQCMNUGAAUNWglO5ShXRImZtUwYFVyH4AczYA6ZYqkeXXgAM0QfGWVgJlY2QMVmRGBSQmIAV0IpRUJPAFoY9njE3IBWXIAGYABiKhkeUgEeDmNIRRDIOng0sj5pNJ5FLo5SfS7EMkpsz0YWdkZBWvh6xrZjYHBwEIypREHCSDUvT2oAClxSNTTkABpIAG9hbPJbwlJlAgBfAEp7qEh-viIfr7LaZXbwfYnHzQ0g8bw-O5-f7-GSENjwDQ+Q6eE4AAzYiFhuxykAAJHdXu8PpAAO6KRCcHqeNLvQiIDjk2HeHwAK3g2ROACJboKvh9cV8ANxI5GA4GQc6XZARC5qE4MxRsW7QnxcthSmUfaUfDZSNRaDgAUQAHm91AREf8AALY47kP7vBmyE5chEylwATS6PEgns4smwHBkcm4fBlqPRmOxQoAkmjFHIw7JRX8TSaNONkNbIABeIjIGmQG12jQnL7gIt+ZBe8gnACMBvAoAgAHk0g5+xwGQQQhjDnScuAANoAGR7AHEALroQUEolTUPN8M0OkMpks5BsjhtwWQadzpcrtNKTNb7NnkDrf5AA)

Now let's take a look at how you can include decorators in your codebase

## Adding Decorators

Decorators are currently not a part of the standard JavaScript language. They are still being discussed in tc39 and have reached [proposal stage 3](https://github.com/tc39/proposal-decorators). This means the spec has more or less stabilized and we can use them but they would be transplied before being run in the browser. This would be done via babel or tsc for most users

Here are the steps to enable decorators using babel:
* Install the `proposal-decorators` plugin
``` shell
npm install --save-dev @babel/plugin-proposal-decorators
```
* Add the plugin to `.babelrc`
``` javascript
{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "version": "2023-05" }]
  ]
}
```
More details are available [here](https://babeljs.io/docs/babel-plugin-proposal-decorators)


In TypeScript, standard decorators will work in principle out of the box. Just make sure:

* To upgrade to TypeScript to version 5 or higher
* Make sure the compiler targets "ES2015"or higher
* Disable or remove the experimentalDecoratorsflag
 
## More examples:

Let's take a look at some other cases where decorators can be useful:

### Abstract away repeated checks

Decorators could be very useful for abstracting repeated checks like Validation, Authentication and Authorization. In this example, we'll create a decorator to validate all the inputs to `strictSum` and `strictMulitply` methods

```javascript
function Validator(value, {kind, name}) {
  return function(...args) {
    const validArgs = args.every(arg => Number.isInteger(arg));
    if (!validArgs) {
      throw new TypeError(`Argument passed to ${name} cannot be a non-integer`);
    }
    console.log(`All arguments passed to ${name} are valid`)
    return value(...args);
  }
}

class MathExample {
  
  @Validator
  strictSum(...args) {
    //add all arguments passed
    let result = args.reduce((acc, val) => acc + val, 0)
    console.log(result)
    return result
  }

  @Validator
  strictMultiply(...args) {
    //multiply all arguments passed
    let result = args.reduce((acc, val) => acc * val, 1)
    console.log(result)
    return result
  }
}
let math = new MathExample()
math.strictSum(1, 2, 3, 4, 5); //Works
math.strictMultiply(1, 2, 3, 4, "5"); //Throws an error
```
You can play around with this code [here](https://www.typescriptlang.org/play?noImplicitAny=false&noImplicitThis=false&noImplicitReturns=false&ssl=17&ssc=1&pln=18&pc=1&experimentalDecorators=false#code/PQKhCgAIUg5B7ALgUwFyQOrIOQDdmQB2SkAhgCbnLlmECekidADsgM6QAWyATgYvEgBrZMmaNukAMbwqZDmwCWAW2YAbAqQ7N4bJQCMNUGAAUNWglO5ShXRImZtUwYFVyH4AczYA6ZYqkeXXgAM0QfGWVgJlY2QMVmRGBSQmIAV0IpRUJPAFoY9njE3IBWXIAGYABiKhkeUgEeDmNIRRDIOng0sj5pNJ5FLo5SfS7EMkpsz0YWdkZBWvh6xrZjYHBwEIypREHCSAA1UjVFcgalgApcY7TkABpIAG8hbPIHwlJlZABfAEonqCQPiIfr7LaZXbwQgXHyw0g8bz-R6AyDSKFscbXE7kACCCI4AF4et4fMh8Dw6Bd4dMCQA+OBpZT6Xg+RRsACShBQnl4VIRv1+AG4Ua12hcAIRY054xEA1HyiRBADuRGQKoAKrMAKI8II8C4AAxljOQXMgzC0bGo80gABJHh8vt9pCliONmWQiFDctlubwDUKRd8RTJCGx4BofGovIacWo1MSTVztJbrQI7Q7Pj8egQpeQAyLgaDIFjbjC4fjA6jg8HwFI1JbIABZBqcLUAD0+6gIyNRgIAAkdseceICMQMdgBlRnlnzUthIkUuZRpNS7dQMY4J6lJxApvTUEUacZ8Nir8ZE+c+PjkNJSZAXKlSKQPLH-OlkZ+QADUJeOD3KX4Q3RCNkCjGNT3PICFSLHh9kgtdAVrVFB2OU4RzHRAJ0QJtzwSNRKVhOdKzleVlzwjcyHjRMvmTc1U3II9kBPdhz0gS98Wvag7wfJ8Xz-NR33pUgvxgLEHgARmg+VQ3DSNo08C4EMQaTUVg+DWMQ6twGDY9IGUVt2NVFUW0QNtO1UDQLiAgyzJ8ccAkQadlAuCSHgAJgeABmB4ABYHhKQNbM4eysMc3C13wyk3MgTzIB8yB-MgAAiEpkqrSBwCAA)

The `Validator` decorator allows us to be confident that the inputs are going to be numbers. Also, by encapsulating the validation logic in the decorator, we have made our methods much more readable and reused the validation code.

We could even write specific conditions for individual methods by checking the `name`. For example, we can check the 0 isn't passed to a divide method.

In my team, we use such decorators to authenticate if a user was the moderator of a meeting and had the right to take actions like turn recording on, mute another participant and so on.

### Measure time taken by a method

Decorators can be used for doing auxiliary actions like debugging without modifying the original code. Here we will create a custom `@measureTime` decorator that logs the execution time of a method:

``` javascript
function measureTime(value, {kind, name}) {
  const originalMethod = value;
  return function (...args) {
    const start = performance.now();
    const result = originalMethod.apply(this, args);
    const end = performance.now();
    console.log(`Execution time for ${name}: ${end - start} milliseconds`);
    return result;
  };
}

class Timer {
  @measureTime
  heavyComputation() {
    // Simulate a heavy computation
    for (let i = 0; i < 1000000000; i++) {}
  }
}

const timer = new Timer();
timer.heavyComputation(); // Logs execution time
```
You can test this code [here](https://www.typescriptlang.org/play?noImplicitAny=false&noImplicitThis=false&noImplicitReturns=false#code/PQKhCgAIUg5B7ALgUwFyQOrIOQDdmQB2SkAhgCbnLlmECekidADsgM6QAWyATgYvEgBrZMmaNukAMbwqZDmwCWAW2YAbAqQ7N4bJQCMNUGAAUNWglO5ShXRImZtUwYFVyH4AczYA6ZYqkeXXgAM0QfGWVgJlY2QMVmRGBSQmIAV0IpRUJPAFoY9njE3IBWXIAGYABiKhkeUgEeDmNIRRDIOng0sj5pNJ5FLo5SfS7EMkpsz0YWdkZBWvh6xrZjYHBwEIypREHCSGVkLX7kABUVZAAKXFI1NOQAGkgAbyFs8ifCUkOAXwBKF5QSB8RD9fZbTK7eD7S4+OGkHjeAHPIGQaTQtjjTEI8YAXkgrB4ISWyhSUmQPmIAHdLn8ANyo9GETHA9hpNR4yA3O4U0jMdR0S6ITiKNhPBFIhlotEyZnjZCEGj4wnEnikzIU6m0qXS2VseAaHxqLyXAAGAFEAB7IKRpKH7XaHSCqyAAEmeX1+6HdCpouUg2J4iB+B0UajUopt0PIbFN9MZILBrLY7MQOp+DJ+Gykai0HHOhx4gLRAAFDsc+AXkEDuKRcHQAMLwVR2hp7WnF6UuSAAZRU7Iami4R3r6JbiDb0MZLsuGnGikg+PKdNakAAPJAAIzlHe73crxQAakPyKzaKzWfAevGjt4i6IyCpkCrPG14FvPB8tfrTfHk8I2qQN2AAyXgcMg1q2vajAXEAA)

## Add caching for computationally intensive methods

Another good use case for Decorators can be optimisation. For example, we can use decorators to cache or memoize the results of a computationally intensive method. Let's modify our MathExample class to see this in action

```javascript
function memoize(value, {kind, name}) {
    let cache = new Map();
    return function(...args) {
      let key = args.join()
      if (cache.has(key)) {
        console.log("returning from cache", cache.get(key))
        return cache.get(key);
      }
      let result = value.apply(this, args); 
      cache.set(key, result);
      return result;
    };
}

class MathExample {
  @memoize
  multiply(...args) {
    //multiply all arguments passed
    let result = args.reduce((acc, val) => acc * val, 1)
    console.log(result)
    return result
  }
}
let math = new MathExample()
math.multiply(1, 2, 3, 4, 5);
math.multiply(1, 2, 3, 4, 5);
math.multiply(1, 2, 3, 4, 5, 6);
math.multiply(1, 2, 3, 4, 5);
math.multiply(1, 2, 3, 4, 5, 6);

/**
 * Output
[LOG]: 120 
[LOG]: "returning from cache",  120 
[LOG]: 720 
[LOG]: "returning from cache",  120 
[LOG]: "returning from cache",  720 
 */

```
You can test this code [here](https://www.typescriptlang.org/play?noImplicitAny=false&noImplicitThis=false&noImplicitReturns=false&ssl=7&ssc=1&pln=44&pc=4&experimentalDecorators=false#code/PQKhCgAIUg5B7ALgUwFyQOrIOQDdmQB2SkAhgCbnLlmECekidADsgM6QAWyATgYvEgBrZMmaNukAMbwqZDmwCWAW2YAbAqQ7N4bJQCMNUGAAUNWglO5ShXRImZtUwYFVyH4AczYA6ZYqkeXXgAM0QfGWVgJlY2QMVmRGBSQmIAV0IpRUJPAFoY9njE3IBWXIAGYABiKhkeUgEeDmNIRRDIOng0sj5pNJ5FLo5SfS7EMkpsz0YWdkZBWvh6xrZjYHBwEIypREHCSGVkZXhFAC9kAApcUjU05AAaSABvIWzyR8JSQ4BfAEpnqCQIEacZSUhWAgAXiIyAA7pAALKkZgXX4AbkBQL4iH6+y2mV28EIFx8pNIPG8-yemKBkBBwmQDGh5O8PgAVidib8aUC2pALmCIT5OFoLiI6L8qTzaTJCGx4BofGovBcAETY3FTSAhILKaTg7iqx6C7g+TzIRBixmS6VYi24-VC82W8Xo6XfaX0vhsNJqcbQ663ZA+ZHqOgXRCcRRsR4stjoyDSk3BtgWq10R7e32IN202kanj7LN+jF574Yj3gKRqLQcJGRgCiAA8vuoCNSgQABQ7HM7IQFsRADHYI7MJNTh0khinxgF5lzKMdhshqNQ9TxpQ6ERDaWvUGle9jZyDMmc+PjkNJSS4XcFSR6B-6QgB8ZCkUmgkEDjwAjNy87K8qKsqngXMWOY0gWRZHn6gIeh69LKA0nAnjC8L1pwzathoqLgEhkY+IOw6IKOfrjuGP6PAATI8ADMjwACyPCUbr4ZwhFDgEJFLhOFyUZANGQPRkBMZALEYmxHHEaRuxhnx1F0YxzGPAAbKxyFSVxMnkfJAmKSJzHqQRRFaTxFEKUJSliapbrgKAECfgA8mkDgueAADaAAyjkAOIALroD+VHlImXm+QFkDqvahZajq8B6smRpAkFIUed5-noAA7MFoXpRFUU4jFOTarqjqGo8kApbl4XoAVmrFXFCUGsgSWQNlqXQMAQA)

## Composition
Decorators have the powerful features of being composed and nested. It means we can apply multiple decorators to the same piece of code, and they’ll execute in a specific order. It helps in building complex and modular applications.

Lets combine `@memoize` and `@mesureTime` decorators to see composition in action

```javascript
function memoize(value, {kind, name}) {
    let cache = new Map();
    return function(...args) {
      let key = args.join()
      if (cache.has(key)) {
        console.log("returning from cache", cache.get(key))
        return cache.get(key);
      }
      let result = value.apply(this, args); 
      cache.set(key, result);
      return result;
    };
}

function measureTime(value, {kind, name}) {
  const originalMethod = value;
  return function (...args) {
    const start = performance.now();
    const result = originalMethod.apply(this, args);
    const end = performance.now();
    console.log(`Execution time for ${name}: ${end - start} milliseconds`);
    return result;
  };
}

function wait(ms) {
  let start = Date.now();
  let now = start
  while (now - start < ms) {
    now = Date.now();
  }
}

class MathExample {
  @measureTime
  @memoize
  multiply(...args) {
    //multiply all arguments passed
    let result = args.reduce((acc, val) => acc * val, 1)
    //wait for 1 seconds
    wait(1000)
    console.log(result)
    return result
  }
}
let math = new MathExample()


math.multiply(1,2,3,4,5);
math.multiply(1,2,3,4,5);

/**
 * NOTE: The time might vary for each run, but note the difference between the memoized respons and the normal execution
[LOG]: 120 
[LOG]: "Execution time for multiply: 1000.5 milliseconds" 
[LOG]: "returning from cache",  120 
[LOG]: "Execution time for multiply: 0.19999980926513672 milliseconds" 
 */
```
You can test this code [here](https://www.typescriptlang.org/play?noImplicitAny=false&noImplicitThis=false&noImplicitReturns=false&experimentalDecorators=false#code/PQKhCgAIUg5B7ALgUwFyQOrIOQDdmQB2SkAhgCbnLlmECekidADsgM6QAWyATgYvEgBrZMmaNukAMbwqZDmwCWAW2YAbAqQ7N4bJQCMNUGAAUNWglO5ShXRImZtUwYFVyH4AczYA6ZYqkeXXgAM0QfGWVgJlY2QMVmRGBSQmIAV0IpRUJPAFoY9njE3IBWXIAGYABiKhkeUgEeDmNIRRDIOng0sj5pNJ5FLo5SfS7EMkpsz0YWdkZBWvh6xrZjYHBwEIypREHCSGVkZXhFAC9kAApcUjU05AAaSABvIWzyR8JSQ4BfAEpnqCQIEacZSUhWAgAXiIyAA7pAALKkZgXX4AbkBQL4iH6+y2mV28EIFx8pNIPG8-yemKBkBBwmQDGh5O8PgAVidib8aUC2pALmCIT5OFoLiI6L8qTzaTJCGx4BofGovBcAETY3FTSAhILKaTg7iqx6C7g+TzIRBixmS6VYi24-VC82W8Xo6XfaX0vhsNJqcbQ663ZA+ZHqOgXRCcRRsR4stjoyDSk3BtgWq10R7e32IN202kanj7LN+jF574Yj2bbaE-aHLT9ZAAFRUl0Dd0eLzeHy+yD+AKBsrY4yWik82RuCItnFkkADNzupcgBbx1b2-NJIYp8f7MqJQ8gQ-J-sgrB4ISWyhSUmDxFhqMXA7342Lx5HY8+aknkdkIeYYYjUYxj0lIPtIT6QMghA0NCp7njwl6ZDe8B3rmu5ygqwbKp4FwAAYAKIAB7IFIaQ1owLbakskAACRPJ8PzoLRkE0LkB6IEe3wHIoahqNGxFEuQbA4ahdo4oWS7sNmi7luAlb4jsa6wqQiiWso27UsCFpsUes6QAAIg0SEoYu9K3rph48IggKwlGGj8mZrEWeMAA8BzqTSZnQgZKA+Le96Ah6lZSGoWgcEikaEV86gEBpkAAAJ1j6fDNocgIJUcJznICyjZgkajhhucZSnmLg5X6eUMDcajAWkhyEIg2ihdQNJepJfq6XGPh8OQaTXhcFzglIjyBv8kIAHxkFIUjQJAgaPAAjNyJXAEpKmUTwkDzQe-FQasearZa83lMdS1ofKipYRcL6naJDovgFsngPSl6RrphBwoiDScJFqgaKiGzgC9nB+Ll-7zfcABM9wAMz3AALPcJRukDIPlWDkMw-DiNuuAoAQDNsAAPKNnh6CNpIuyHFxnicOM1w8AwcEQQaS4ZI8+ikUQSD8JI5BtCEvCQdekD6BasKiPskYEIcxxnNQElsDocq0DQUtc-BNwQURJE1uAADaAAyhMAOIALroPNEPlImhsm+bkCqoRxGkWulMEEzZW7GGFvHeUPglFxPF8bKgmqjbRtm+g6r2oWWo6vAerJkaQKW9b+sR-bjvay7RLkVTHug-l6B+-NACc5flwAHOUpcQwAbCU83Q3XADsEOB7xqYh2wYdrEAA)

There are many more such use-cases like:

* **Retry Mechanism**. We can create decorators that automatically retry a method certain number of times in case of failures.

* **Event Handling**. Decorators can trigger events before and after a method’s execution, enabling event-driven architectures.

* **Async cases**. Decorators can also be used for asynchronous cases like debounce and throttle.

* **Class/property decorators**. Decorators can also be applied on classes or individual properties. We've not covered these here, but they work in a similar manner. Please check the [proposal](https://github.com/tc39/proposal-decorators?tab=readme-ov-file) for more details

## Further reading

- [tc39 decorators proposal](https://github.com/tc39/proposal-decorators?tab=readme-ov-file)
- [Decorators documentation in TypeScript] (https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#decorators)
- [Babel plugin](https://babeljs.io/docs/babel-plugin-proposal-decorators)
- [Sitepoint article](https://www.sitepoint.com/javascript-decorators-what-they-are/) - this article uses the older syntax for decorators, but explains the concepts well

I hope that this post demonstrates the flexibility of the decorator design pattern and encourages you to integrate it into your codebase. It's important to note that decorators may not be suitable for every use case. As a general guideline, avoid placing crucial business logic within decorators; instead, utilize them to encapsulate supplementary code.