Exploring the Power of Typescript Decorators: Real-World Examples and Best Practices
Tags: typescript, javascript, webdev
Published: 2022-12-20 21:31:20

> Follow me: [Twitter](https://twitter.com/aymenkkani)

Typescript decorators are a powerful feature that allows developers to add additional behavior to class declarations, methods, and properties. In this tutorial, we'll explore what decorators are and how to use them in your Typescript projects.

## What are decorators?

Decorators are functions that are called with a specific `target`, `property`, `descriptor`, or `parameter` as their arguments. They can be used to modify the behavior of the decorated item in various ways.

## How to use decorators

To use a decorator in your Typescript code, you first need to enable the `experimentalDecorators` compiler option in your `tsconfig.json` file:

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

Once this option is enabled, you can use decorators by placing the `@` symbol followed by the decorator name before the item you want to decorate.

For example, here's how you can use the `@sealed` decorator to seal a class, making it non-extendable:

```javascript
@sealed
class MyClass {
  // class implementation here
}

class AnotherClass extends MyClass {
  // this will cause a compile-time error because MyClass is sealed
}
```

## Creating your own decorators

In addition to using the built-in decorators, you can also create your own decorators by defining a function with the decorator signature.

Here's an example of a custom `@log` decorator that logs the arguments and return value of a decorated method:

```javascript
function log(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function(...args: any[]) {
    console.log(`Arguments: ${args}`);
    const result = originalMethod.apply(this, args);
    console.log(`Result: ${result}`);
    return result;
  };
  return descriptor;
}

class MyClass {
  @log
  add(x: number, y: number): number {
    return x + y;
  }
}

const myClass = new MyClass();
myClass.add(1, 2); // will log "Arguments: 1,2" and "Result: 3"
```

> To use the `log` decorator, it is first defined as a function that takes a `target`, `propertyKey`, and `descriptor` as its arguments. The `target` argument represents the object containing the decorated method, the `propertyKey` argument represents the name of the method, and the `descriptor` argument represents the property descriptor of the method.

In this example, the `log` decorator stores a reference to the original method in a variable called `originalMethod`, and then replaces the method implementation with a new function that logs the arguments and result before calling the original method and returning its result.

Finally, the decorator returns the modified `descriptor` object.

## Real-world examples

Now that we've seen how decorators work, let's look at some real-world examples of how they can be used.

**Caching**

One common use case for decorators is caching the result of a method so that it doesn't have to be recomputed every time it's called. Here's an example of a `@cache` decorator that does this:

```javascript
function cache(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  let cache: any;
  descriptor.value = function(...args: any[]) {
    if (!cache) {
      cache = originalMethod.apply(this, args);
    }
    return cache;
  };
  return descriptor;
}

class MyClass {
  @cache
  expensiveComputation(x: number): number {
    // expensive computation here
  }
}

const myClass = new MyClass();
myClass.expensiveComputation(1); // will compute the result
myClass.expensiveComputation(1); // will return the cached result
```

This code defines a `cache` decorator that can be applied to methods to cache their results. When the decorated method is called, the decorator checks if a cache already exists for the result. If it does, the cached result is returned. If it doesn't, the method is called and its result is cached for future calls.

**Validation**

Decorators can also be used to validate the input of a method or the state of an object. Here's an example of a `@validate` decorator that checks if the input of a method is a non-empty string:

```javascript
function validate(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function(...args: any[]) {
    if (typeof args[0] !== 'string' || !args[0]) {
      throw new Error('Invalid input');
    }
    return originalMethod.apply(this, args);
  };
  return descriptor;
}

class MyClass {
  @validate
  processString(str: string) {
    // process string here
  }
}

const myClass = new MyClass();
myClass.processString('foo'); // valid input
myClass.processString(''); // will throw an error
```

In this example, the `validate` decorator replaces the method implementation with a function that checks the input and throws an error if it is invalid. If the input is valid, the original method is called.

## Conclusion

Decorators are a powerful feature of Typescript that allow developers to add additional behavior to class declarations, methods, and properties. They are useful for tasks such as caching, validation, and logging.

In this tutorial, we've seen how to use and create decorators in Typescript, and we've looked at some real-world examples of how they can be used. Whether you're new to decorators or an experienced developer, I hope this tutorial has provided you with a good understanding of how they work and how you can use them in your projects.

> Follow me: [Twitter](https://twitter.com/aymenkkani)