Decorators in TypeScript provide a way to add annotations and a meta-programming syntax for class declarations and members. They are a powerful feature that allows you to modify the behavior of classes, methods, accessors, properties, or parameters. In this section, we will explore how decorators work, their types, and how to implement them in your TypeScript projects.

Key Concepts

  1. What are Decorators?

    • Decorators are special types of declarations that can be attached to a class, method, accessor, property, or parameter.
    • They are a form of syntactic sugar that allows you to apply reusable logic to your code.
  2. Types of Decorators:

    • Class Decorators: Applied to a class constructor.
    • Method Decorators: Applied to a method of a class.
    • Accessor Decorators: Applied to a getter or setter of a class.
    • Property Decorators: Applied to a property of a class.
    • Parameter Decorators: Applied to a parameter of a method.
  3. Decorator Factories:

    • A decorator factory is a function that returns a decorator function. It allows you to pass parameters to a decorator.

Practical Examples

Class Decorator

A class decorator is a function that takes a class constructor as an argument and can return a new constructor to replace the original one.

function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return `Hello, ${this.greeting}`;
  }
}

Explanation:

  • The sealed decorator seals the constructor and its prototype, preventing new properties from being added.

Method Decorator

A method decorator is a function that is applied to the property descriptor of the method.

function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey} with arguments: ${args}`);
    return originalMethod.apply(this, args);
  };
}

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

const calculator = new Calculator();
calculator.add(2, 3); // Logs: Calling add with arguments: 2,3

Explanation:

  • The log decorator wraps the original method, logging its name and arguments before calling it.

Property Decorator

A property decorator is a function that is applied to a property of a class.

function readonly(target: any, propertyKey: string) {
  Object.defineProperty(target, propertyKey, {
    writable: false,
  });
}

class Person {
  @readonly
  name: string = "John Doe";
}

const person = new Person();
person.name = "Jane Doe"; // Error: Cannot assign to read only property 'name'

Explanation:

  • The readonly decorator makes the name property immutable.

Exercises

Exercise 1: Create a Method Decorator

Task: Create a method decorator named measureTime that logs the execution time of a method.

function measureTime(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    const start = performance.now();
    const result = originalMethod.apply(this, args);
    const end = performance.now();
    console.log(`Execution time for ${propertyKey}: ${end - start}ms`);
    return result;
  };
}

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

const operations = new MathOperations();
operations.multiply(5, 10);

Solution Explanation:

  • The measureTime decorator calculates the time taken to execute the multiply method and logs it.

Exercise 2: Implement a Class Decorator

Task: Implement a class decorator named logClass that logs the creation of an instance of the class.

function logClass(constructor: Function) {
  const original = constructor;
  function construct(constructor: any, args: any[]) {
    console.log(`Creating instance of ${constructor.name}`);
    return new original(...args);
  }
  const newConstructor: any = function (...args: any[]) {
    return construct(original, args);
  };
  newConstructor.prototype = original.prototype;
  return newConstructor;
}

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

const user = new User("Alice");

Solution Explanation:

  • The logClass decorator logs a message every time a new instance of the User class is created.

Conclusion

Decorators in TypeScript provide a powerful way to add behavior to your classes and their members. By understanding and utilizing decorators, you can write more modular and reusable code. In the next section, we will explore best practices for using TypeScript with Playwright, building on the knowledge of decorators and other advanced TypeScript features.

© Copyright 2024. All rights reserved