DEV Community

chirag pipaliya
chirag pipaliya

Posted on

Mastering TypeScript 5.0 Decorators: The Ultimate Guide

Introduction:

TypeScript has been gaining popularity due to its ability to enhance JavaScript with static typing and support for modern features like classes. One of the most powerful features of TypeScript is decorators. Decorators allow developers to add metadata or modify the behavior of classes, methods, properties, and parameters, making it easier to write and maintain large-scale applications.

In this guide, we'll cover everything you need to know about TypeScript decorators, including how they work, their common use cases, and advanced techniques to help you master them. We'll also provide practical examples to demonstrate how to implement and use decorators in real-world projects.

Understanding TypeScript Decorators:
In TypeScript, a decorator is a special kind of declaration that can be attached to a class declaration, method, property, or parameter. A decorator consists of an @ symbol followed by a function that will be executed at runtime with information about the declaration it decorates.

Here's an example of a simple decorator:

function log(target: any, key: string, descriptor: PropertyDescriptor) {
  console.log(`Called ${key}()`);
}
Enter fullscreen mode Exit fullscreen mode

This decorator logs the name of the method being called when it is invoked. To use it, we can apply the @log decorator to a method like this:

class Example {
  @log
  foo() {}
}
Enter fullscreen mode Exit fullscreen mode

Now, whenever foo() is called, the decorator function log() will execute and print "Called foo()".

Using TypeScript Decorators:
While it's possible to create decorators for any declaration, there are some common use cases where decorators are particularly useful. Here are a few examples:

  • Logging: As shown above, decorators can be used to log method calls.
  • Validation: Decorators can be used to validate method parameters or object properties before they're set.
  • Caching: Decorators can be used to cache the results of function calls to improve performance.
  • Authorization: Decorators can be used to restrict access to methods based on user roles or permissions.

Here's an example of a decorator that validates the arguments passed to a function:

function validate(target: any, key: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    if (args.length < 2) {
      throw new Error('Expected at least two arguments');
    }
    originalMethod.apply(this, args);
  };
  return descriptor;
}
Enter fullscreen mode Exit fullscreen mode

This decorator checks that any function it is applied to has at least two arguments. If not, it throws an error. To use it, we can apply the @validate decorator to a method like this:

class Example {
  @validate
  foo(a: number, b: number) {
    console.log(a + b);
  }
}
Enter fullscreen mode Exit fullscreen mode

Advanced Techniques with TypeScript Decorators:
In addition to the common use cases mentioned above, there are some advanced techniques you can use to make your decorators even more powerful.

For example, you can use multiple decorators on the same declaration by chaining them together:

class Example {
  @log
  @validate
  foo(a: number, b: number) {
    console.log(a + b);
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, both the @log and @validate decorators will be executed when foo() is called.

You can also create custom decorators to fit your specific needs:

function memoize() {
  const cache: Map<string, any> = new Map();
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const cacheKey = `${key}:${args.join(',')}`;
      if (cache.has(cacheKey)) {
        return cache.get(cacheKey);
      }
      const result = originalMethod.apply(this, args);
      cache.set(cacheKey, result);
      return result;
    };
    return descriptor;
  };
}
Enter fullscreen mode Exit fullscreen mode

This decorator caches the results of function calls by storing them in a Map. To use it, we can apply the @memoize decorator to a method like this:

class Example {
  @memoize
  fib(n: number): number {
    if (n < 2) {
      return n;
    }
    return this.fib(n - 1) + this.fib(n - 2);
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, you can use decorator factories to create decorators with configurable options:

function logWithPrefix(prefix: string) {
  return function (target: any, key:: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      console.log(`[${prefix}] Before ${key}()`);
      const result = originalMethod.apply(this, args);
      console.log(`[${prefix}] After ${key}()`);
      return result;
    };
    return descriptor;
  };
}
Enter fullscreen mode Exit fullscreen mode

In this example, the logWithPrefix() decorator factory returns a new decorator function that logs method calls with a prefix specified as an argument. To use it, we can create a new instance of the decorator with a specific prefix and apply it to a method like this:

class Example {
  @logWithPrefix('MyClass')
  foo() {}
}
Enter fullscreen mode Exit fullscreen mode

Now, whenever foo() is called, the decorator will log "Before foo()" and "After foo()" with the prefix "[MyClass]" to indicate which class the method belongs to.

TypeScript Decorators in Action:
To see TypeScript decorators in action, let's look at a practical example. Suppose we're building a web application that requires authentication for certain routes. We could create an @auth decorator that checks if the user is authenticated before allowing access to a route:

function auth(target: any, key: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    const isAuthenticated = checkIfUserIsAuthenticated();
    if (!isAuthenticated) {
      // Redirect to login page or show error message
      return;
    }
    originalMethod.apply(this, args);
  };
  return descriptor;
}
Enter fullscreen mode Exit fullscreen mode

Now, we can apply the @auth decorator to any method that requires authentication:

class MyRoutes {
  @auth
  getProfile() {
    // Return user profile data
  }

  @auth
  updateProfile(profileData: any) {
    // Update user profile data
  }
}
Enter fullscreen mode Exit fullscreen mode

With this setup, the @auth decorator will automatically check if the user is authenticated before allowing access to the getProfile() and updateProfile() methods.

Conclusion:
TypeScript decorators are a powerful tool that can help you write cleaner, more maintainable code. In this guide, we covered everything you need to know about TypeScript decorators, from their basic syntax to advanced techniques like decorator factories. We also provided practical examples to show how you can use decorators to solve real-world problems. With this knowledge, you'll be able to take your TypeScript skills to the next level and build better applications.

Top comments (0)