Illustration of JavaScript and TypeScript decorators showcasing their application in enhancing code functionality.

JavaScript and TypeScript Decorators: A Complete Guide with Examples

· 3 min read

Decorators in JavaScript and TypeScript provide a powerful way to modify or extend the behavior of classes, methods, properties, and parameters. They simplify repetitive tasks such as logging, validation, and caching, and are widely used in frameworks like Angular, NestJS, and TypeORM.

In this guide, we’ll cover:

  1. What are decorators, and why should you use them?
  2. Types of decorators with practical examples.
  3. How to implement advanced use cases with reflect-metadata.
  4. Common pitfalls and best practices.

What Are Decorators?

A decorator is a function that adds behavior to a class, method, property, or parameter. Think of it as a wrapper that enhances functionality without altering the original implementation.

Why Use Decorators?

  1. Code Reusability: Abstract repetitive tasks into reusable decorators.
  2. Readability: Use expressive syntax to define cross-cutting concerns (e.g., logging, validation).
  3. Framework Compatibility: Decorators are integral to frameworks like NestJS, Angular, and TypeORM.
  4. Maintainability: Keep business logic clean by separating additional behaviors.

Types of Decorators with Examples

1. Function Decorators

Function decorators wrap standalone functions to modify their behavior.

Example: Adding Execution Time Logging

function withExecutionTime(fn: Function) {
  return function (...args: any[]) {
    console.time('Execution Time');
    const result = fn(...args);
    console.timeEnd('Execution Time');
    return result;
  };
}

// Original function
function calculateSum(a: number, b: number) {
  return a + b;
}

// Wrap function with the decorator
const calculateSumWithTiming = withExecutionTime(calculateSum);

console.log(calculateSumWithTiming(5, 10));

// Output:
// Execution Time: 0.123ms
// 15



2. Class Decorators

Class decorators modify or enhance an entire class.

Example: Tracking Instances

function TrackInstances(constructor: Function) {
  let count = 0;

  return class extends constructor {
    constructor(...args: any[]) {
      super(...args);
      count++;
      console.log(`Instance count: ${count}`);
    }
  };
}

@TrackInstances
class User {
  constructor(public name: string) {}
}

new User('Alice'); // Instance count: 1
new User('Bob');   // Instance count: 2


3. Method Decorators

Method decorators add functionality to specific methods.

Example: Logging Method Calls

function LogMethod(target: any, methodName: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Method ${methodName} called with arguments:`, args);
    return originalMethod.apply(this, args);
  };
}

class Calculator {
  @LogMethod
  add(a: number, b: number) {
    return a + b;
  }
}

const calc = new Calculator();
console.log(calc.add(10, 5));

// Output:
// Method add called with arguments: [10, 5]
// 15


4. Property Decorators

Property decorators attach metadata or behaviors to class properties.

Example: Marking Required Properties

import 'reflect-metadata';

function Required(target: any, propertyKey: string) {
  Reflect.defineMetadata('required', true, target, propertyKey);
}

function Validate(instance: any) {
  for (const key in instance) {
    if (Reflect.getMetadata('required', instance, key) && !instance[key]) {
      throw new Error(`${key} is required`);
    }
  }
}

class User {
  @Required
  name: string;

  constructor(name: string) {
    this.name = name;
    Validate(this);
  }
}

new User('Alice'); // Works fine
new User(''); // Throws: name is required


5. Parameter Decorators

Parameter decorators add metadata to method parameters.

Example: Validating Parameters

import 'reflect-metadata';

function RequiredParam(target: any, methodName: string, paramIndex: number) {
  const existingParams = Reflect.getMetadata('requiredParams', target, methodName) || [];
  existingParams.push(paramIndex);
  Reflect.defineMetadata('requiredParams', existingParams, target, methodName);
}

function ValidateParams(target: any, methodName: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    const requiredParams = Reflect.getMetadata('requiredParams', target, methodName) || [];
    requiredParams.forEach((index: number) => {
      if (args[index] === undefined) {
        throw new Error(`Missing required parameter at index ${index}`);
      }
    });
    return originalMethod.apply(this, args);
  };
}

class MathOperations {
  @ValidateParams
  multiply(@RequiredParam a: number, b: number) {
    return a * b;
  }
}

const math = new MathOperations();
console.log(math.multiply(10, 5)); // Works
console.log(math.multiply(undefined, 5)); // Throws: Missing required parameter at index 0


6. Accessor Decorators

Accessor decorators modify getters and setters.

Example: Caching Getter Results

function Cache(target: any, key: string, descriptor: PropertyDescriptor) {
  const originalGetter = descriptor.get;
  const cacheKey = `__${key}_cache`;

  descriptor.get = function () {
    if (!this[cacheKey]) {
      this[cacheKey] = originalGetter?.apply(this);
    }
    return this[cacheKey];
  };
}

class ExpensiveComputation {
  private counter = 0;

  @Cache
  get compute() {
    console.log('Computing...');
    return ++this.counter;
  }
}

const obj = new ExpensiveComputation();
console.log(obj.compute); // Computing... 1
console.log(obj.compute); // 1 (cached)


Framework Use Cases

1. NestJS

  • @Controller, @Get, @Post: Define routes.
  • @Injectable, @Module: Manage dependencies.

2. TypeORM

  • @Entity, @Column, @PrimaryGeneratedColumn: Define database models.

3. Angular

  • @Component, @Directive: Define components and directives.

Best Practices for Using Decorators

  1. Avoid Overuse: Use decorators judiciously to avoid clutter.
  2. Follow Standards: Use reflect-metadata for advanced use cases.
  3. Document Well: Provide clear documentation for custom decorators.
  4. Test Thoroughly: Ensure decorators behave as expected in edge cases.

Conclusion

Decorators are an essential tool in TypeScript and JavaScript, simplifying complex behaviors and enhancing code readability. Whether you’re building APIs with NestJS, managing databases with TypeORM, or creating UI components in Angular, understanding decorators unlocks powerful possibilities in modern development.

Explore robust solutions with Cybermindworks. Contact us to bring your ideas to life!

Further Reading

Rishaba Priyan

About Rishaba Priyan

Rishaba Priyan: Frontend Developer | Crafting Seamless User Experiences

At CyberMind Works, Rishaba Priyan excels as a Frontend Developer, specializing in creating intuitive and engaging user interfaces. Leveraging his expertise in technologies like Next.js, Rishaba focuses on delivering seamless digital experiences that blend aesthetics with functionality.

Man with headphones

CONTACT US

How can we at CMW help?

Want to build something like this for yourself? Reach out to us!

Link copied
Copyright © 2024 CyberMind Works. All rights reserved.