Mastering TypeScript 5.0 Decorators: The Ultimate Guide
Tags: typescript, decorators, beginners, programming
Published: 2023-04-26 04:18:35



### **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:

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

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:

```typescript
class Example {
  @log
  foo() {}
}
```

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:

```typescript
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;
}
```

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:

```typescript
class Example {
  @validate
  foo(a: number, b: number) {
    console.log(a + b);
  }
}
```

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:

```typescript
class Example {
  @log
  @validate
  foo(a: number, b: number) {
    console.log(a + b);
  }
}
```

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:

```typescript
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;
  };
}
```

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:

```typescript
class Example {
  @memoize
  fib(n: number): number {
    if (n < 2) {
      return n;
    }
    return this.fib(n - 1) + this.fib(n - 2);
  }
}
```

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

```typescript
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;
  };
}
```

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:

```typescript
class Example {
  @logWithPrefix('MyClass')
  foo() {}
}
```

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:

```typescript
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;
}
```

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

```typescript
class MyRoutes {
  @auth
  getProfile() {
    // Return user profile data
  }
  
  @auth
  updateProfile(profileData: any) {
    // Update user profile data
  }
}
```

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.