DEV Community

ayka.code
ayka.code

Posted on

Exploring the Power of Typescript Decorators: Real-World Examples and Best Practices

Follow me: Twitter

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

@sealed
class MyClass {
  // class implementation here
}

class AnotherClass extends MyClass {
  // this will cause a compile-time error because MyClass is sealed
}
Enter fullscreen mode Exit fullscreen mode

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:

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"
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)