Beyond Decorators: Alternative Techniques for Code Enhancement in TypeScript

2024-07-27

In TypeScript, decorators are a powerful syntactic feature that allows you to add annotations and metaprogramming capabilities to classes, methods, properties, accessors, and parameters. They are executed during compilation, providing a way to modify or extend the behavior of your code at runtime.

Creating a TypeScript Decorator

Here's a breakdown of how to create a TypeScript decorator:

  1. @myDecorator
    class MyClass {
        // ...
    }
    

There are four main categories of TypeScript decorators:

  • Class Decorators: Applied to class declarations, they can be used for logging, validation, or adding metadata.
  • Method Decorators: Attached to methods, they can perform tasks like logging, authorization, or method binding.
  • Property Decorators: Decorate properties to add functionalities like validation or type information.
  • Parameter Decorators: Less common, they can be used for type checking or dependency injection on function parameters.

Example: Logging Decorator

Let's create a simple decorator that logs method calls:

function logMethod(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value; // Store the original method

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

    return descriptor;
}

class MyClass {
    @logMethod
    greet(name: string) {
        console.log(`Hello, ${name}!`);
    }
}

const obj = new MyClass();
obj.greet('Alice'); // Logs the method call and its arguments

In this example, the logMethod decorator is applied to the greet method. It logs the method name and its arguments before calling the original method.

Key Points to Remember

  • Decorators are processed during compilation, not at runtime.
  • They provide a clean and concise syntax for adding annotations and metaprogramming to your code.
  • Use decorators judiciously to avoid making your code overly complex or hard to understand.



This example shows a decorator factory that creates a class decorator for logging class instantiation:

function withLog(message: string) {
  return function (target: Function) { // Decorator factory
    console.log(`${message} class: ${target.name}`);
    return target;
  };
}

@withLog('Creating')
class MyClass {
  // ...
}

const myInstance = new MyClass(); // Logs "Creating class: MyClass"

Method Decorator (for Authorization):

This example defines a decorator that checks for an authorization token before allowing a method call:

function authorized(token: string) {
  return function (target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      if (this.authToken === token) {
        return originalMethod.apply(this, args);
      } else {
        throw new Error('Unauthorized access');
      }
    };

    return descriptor;
  };
}

class User {
  authToken: string;

  constructor(token: string) {
    this.authToken = token;
  }

  @authorized('secret_token')
  getData() {
    return 'This is sensitive data!';
  }
}

const user = new User('secret_token');
console.log(user.getData()); // Returns "This is sensitive data!"

const unauthorizedUser = new User('wrong_token');
unauthorizedUser.getData(); // Throws "Unauthorized access" error

Property Decorator (for Validation):

This example shows a decorator that validates the minimum length of a property:

function minLength(length: number) {
  return function (target: Object, propertyKey: string) {
    let value: string;

    const descriptor: PropertyDescriptor = {
      get() {
        return value;
      },
      set(newValue: string) {
        if (newValue.length < length) {
          throw new Error(`Property "${propertyKey}" must be at least ${length} characters long.`);
        }
        value = newValue;
      }
    };

    return descriptor;
  };
}

class Product {
  @minLength(5)
  name: string;

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

const product = new Product('LongEnoughName');
console.log(product.name); // "LongEnoughName"

const invalidProduct = new Product('Short'); // Throws "Property "name" must be at least 5 characters long." error



  • HOFs are functions that take another function as an argument and/or return a new function.
  • You can achieve decorator-like behavior by wrapping the target element (class, method, property) in an HOF.
  • HOFs offer more flexibility for dynamic behavior based on arguments.

Example (Logging Method Calls):

function logMethod(method: Function) {
  return function (...args: any[]) {
    console.log(`Method ${method.name} called with arguments:`, args);
    return method.apply(this, args);
  };
}

class MyClass {
  greet(name: string) {
    console.log(`Hello, ${name}!`);
  }
}

const MyClassWithLogging = logMethod(MyClass.prototype.greet); // HOF wrapping

const obj = new MyClass();
MyClassWithLogging.call(obj, 'Alice'); // Logs the method call and its arguments

Mixins:

  • Mixins are objects that contain reusable functionality.
  • You can create mixins with the desired logic (e.g., logging, authorization) and extend your classes with them.
  • Mixins promote code reusability and separation of concerns.

Example (Adding Logging to a Class):

const LoggingMixin = {
  logMethod(method: Function) {
    return function (...args: any[]) {
      console.log(`Method ${method.name} called with arguments:`, args);
      return method.apply(this, args);
    };
  }
};

class MyClass {
  greet(name: string) {
    console.log(`Hello, ${name}!`);
  }
}

Object.assign(MyClass.prototype, LoggingMixin); // Extend with mixin

const obj = new MyClass();
obj.greet('Alice'); // Logs the method call and its arguments

Inheritance:

  • For specific functionalities, creating base classes with the desired behavior can be an option.
  • Subclasses inherit these functionalities and can override them as needed.

Example (Base Class for Authorization):

class Authorizable {
  protected isAuthorized(token: string): boolean {
    return token === 'secret_token'; // Replace with actual authorization logic
  }

  getData() {
    if (this.isAuthorized('secret_token')) {
      return 'This is sensitive data!';
    } else {
      throw new Error('Unauthorized access');
    }
  }
}

class User extends Authorizable {
  // ... other user functionality
}

const user = new User();
console.log(user.getData()); // Returns "This is sensitive data!" (assuming valid token)

Choosing the Right Method:

  • Decorators offer a clean syntax and type safety, but may not be supported in older JavaScript environments.
  • HOFs provide flexibility but require manual wrapping and might be less readable.
  • Mixins promote code reusability but can lead to complex object hierarchies.
  • Inheritance is suitable for creating common behavior across related classes.

typescript decorator



Understanding Getters and Setters in TypeScript with Example Code

Getters and SettersIn TypeScript, getters and setters are special methods used to access or modify the values of class properties...


Taming Numbers: How to Ensure Integer Properties in TypeScript

Type Annotation:The most common approach is to use type annotations during class property declaration. Here, you simply specify the type of the property as number...


Mastering the Parts: Importing Components in TypeScript Projects

Before you import something, it needs to be exported from the original file. This makes it available for other files to use...


Understanding Type Safety and the 'value' Property in TypeScript

In TypeScript, the error arises when you attempt to access a property named value on a variable or expression that's typed as HTMLElement...


Defining TypeScript Callback Types: Boosting Code Safety and Readability

A callback is a function that's passed as an argument to another function. The receiving function can then "call back" the passed function at a later point...



typescript decorator

Understanding TypeScript Constructors, Overloading, and Their Applications

Constructors are special functions in classes that are called when you create a new object of that class. They're responsible for initializing the object's properties (variables) with starting values


Setting a New Property on window in TypeScript

Direct Assignment:The most straightforward method is to directly assign a value to the new property:This approach creates a new property named myNewProperty on the window object and assigns the string "Hello


Understanding Dynamic Property Assignment in TypeScript

Understanding the Concept:In TypeScript, objects are collections of key-value pairs, where keys are property names and values are the corresponding data associated with those properties


TypeScript Object Literal Types: Examples

Type Definitions in Object LiteralsIn TypeScript, object literals can be annotated with type definitions to provide more precise and informative code


Example of Class Type Checking in TypeScript

Class Type Checking in TypeScriptIn TypeScript, class type checking ensures that objects adhere to the defined structure of a class