Hone logo
Hone
Problems

Decorator Composition in TypeScript

Decorator composition is a powerful pattern in TypeScript that allows you to chain multiple decorators together, applying their effects sequentially. This challenge asks you to implement a utility function that facilitates decorator composition, making it easier to apply multiple decorators to a class or function without repetitive code. This is useful for building modular and reusable decoration logic.

Problem Description

You need to implement a function called composeDecorators that takes an array of decorators and returns a single decorator that is the composition of all the input decorators. The composed decorator should apply each decorator in the input array sequentially, from left to right. This means the first decorator in the array is applied first, then the second, and so on.

What needs to be achieved:

  • Create a function composeDecorators(decorators: Decorator[]): Decorator that accepts an array of decorators.
  • Return a single decorator that represents the composition of all input decorators.
  • The composed decorator should apply each input decorator in the order they appear in the array.

Key Requirements:

  • The input decorators array can be empty. In this case, the composed decorator should return the original function/class unchanged.
  • The input decorators array can contain any valid TypeScript decorators.
  • The composed decorator should correctly apply the effects of all input decorators.

Expected Behavior:

When the composed decorator is applied to a class or function, it should behave as if each individual decorator was applied sequentially. The order of application is crucial.

Edge Cases to Consider:

  • Empty decorators array: Should return the original function/class.
  • Decorators that modify the prototype vs. decorators that modify the function/class itself.
  • Decorators that return a new function/class vs. decorators that modify the existing one.

Examples

Example 1:

// Define some simple decorators
const addProperty = (property: string) => (target: any) => {
  target.prototype[property] = () => { console.log(`Property ${property} called`); };
};

const logMethod = (constructor: any) => {
  const original = constructor.prototype;
  constructor.prototype = function(...args: any[]) {
    console.log("Method called:", args);
    return Reflect.apply(original, this, args);
  };
};

// Compose the decorators
const composedDecorator = composeDecorators([addProperty('myMethod'), logMethod]);

// Apply the composed decorator to a class
@composedDecorator
class MyClass {
  someMethod() {
    console.log("Some method called");
  }
}

// Create an instance and call the methods
const instance = new MyClass();
instance.someMethod();
instance.myMethod();

Output:

Method called: []
Some method called
Property myMethod called

Explanation: logMethod is applied first, logging the call to someMethod. Then, addProperty adds myMethod to the prototype. Finally, calling myMethod executes its logic.

Example 2:

// Define a simple decorator
const double = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
  const originalMethod = descriptor.value;
  descriptor.value = function(...args: any[]) {
    return originalMethod.apply(this, args) * 2;
  };
  return descriptor;
};

// Compose with an identity decorator (does nothing)
const identity = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => descriptor;

const composedDecorator = composeDecorators([identity, double]);

@composedDecorator
class MathOperations {
  add(a: number, b: number) {
    return a + b;
  }
}

const math = new MathOperations();
console.log(math.add(2, 3));

Output:

10

Explanation: identity does nothing, so the double decorator is applied, effectively doubling the result of the add method.

Example 3: (Edge Case - Empty Decorator Array)

const identity = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => descriptor;

const composedDecorator = composeDecorators([]);

@composedDecorator
class EmptyTest {
  test() {
    console.log("Test method");
  }
}

const instance = new EmptyTest();
instance.test();

Output:

Test method

Explanation: Because the decorator array is empty, the original class EmptyTest is returned unchanged, and the test method executes as expected.

Constraints

  • The composeDecorators function must be implemented in TypeScript.
  • The input decorators array can contain a maximum of 10 decorators.
  • The decorators in the input array can be of any valid TypeScript decorator type (class decorators, method decorators, property decorators, etc.).
  • The function should be performant enough to handle a reasonable number of decorators (up to 10) without significant overhead.

Notes

  • Consider using a recursive approach to compose the decorators.
  • Think about how to handle different types of decorators (class, method, property).
  • The order of decorator application is critical. Ensure your implementation respects this order.
  • The Reflect API might be useful for manipulating the prototype and descriptor objects.
Loading editor...
typescript