Hone logo
Hone
Problems

Crafting Type-Safe Decorators in TypeScript

Decorators in TypeScript provide a powerful way to modify classes, methods, properties, or parameters. However, ensuring type safety within decorators can be tricky. This challenge focuses on creating decorators that maintain type integrity, preventing common pitfalls and ensuring your decorated code remains robust and predictable.

Problem Description

You are tasked with creating a set of type-safe decorators in TypeScript. Specifically, you need to implement three decorators: validate, log, and memoize.

  • validate(validator): This decorator should take a validator function as an argument. The validator function should accept the property value and return a boolean indicating whether the value is valid. If the validator returns false, the decorated property should be set to a default value (provided as the second argument to the decorator).
  • log(prefix): This decorator should take a prefix string as an argument. When the decorated method is called, it should log a message to the console containing the prefix, the method name, and the arguments passed to the method. The original method should then be executed, and its return value should be returned.
  • memoize(): This decorator should memoize the return value of the decorated function based on its arguments. Subsequent calls with the same arguments should return the cached result.

Key Requirements:

  • Type Safety: The decorators must be type-safe, meaning they should correctly infer and maintain the types of the decorated elements.
  • Flexibility: The decorators should be flexible enough to work with various types and scenarios.
  • Correctness: The decorators must function as described above, providing the expected behavior.
  • Readability: The code should be well-structured and easy to understand.

Expected Behavior:

  • validate should correctly validate property values and set default values when validation fails.
  • log should correctly log messages with the specified prefix and arguments.
  • memoize should correctly cache and return memoized values.

Edge Cases to Consider:

  • What happens if the validator function throws an error?
  • How should the log decorator handle methods with no arguments?
  • How should the memoize decorator handle functions that return complex objects? (Consider shallow comparison for simplicity)
  • What happens if the decorated property is already initialized?

Examples

Example 1: validate

class Person {
  @validate("string", "Unknown")
  name: string;

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

const person = new Person("Alice");
console.log(person.name); // Output: Alice

const person2 = new Person(123 as any); // Intentionally invalid input
console.log(person2.name); // Output: Unknown

Explanation: The name property is validated. When an invalid input (number) is provided, the property is set to the default value "Unknown".

Example 2: log

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

const calc = new Calculator();
const result = calc.add(5, 3);
// Output: Calculation: add 5 3
// Output: 8

Explanation: The add method is decorated with log. When called, a message is logged to the console before the method executes.

Example 3: memoize

class ExpensiveService {
  @memoize()
  calculate(input: string): number {
    console.log("Calculating..."); // Simulate expensive operation
    return input.length * 2;
  }
}

const service = new ExpensiveService();
console.log(service.calculate("hello")); // Output: Calculating... 10
console.log(service.calculate("hello")); // Output: 10 (cached)
console.log(service.calculate("world")); // Output: Calculating... 5

Explanation: The calculate method is memoized. The first call triggers the calculation and caches the result. Subsequent calls with the same input return the cached result without re-executing the calculation.

Constraints

  • TypeScript Version: Use TypeScript 4.0 or higher.
  • Validator Function: The validator function must accept a single argument (the property value) and return a boolean.
  • Memoization Comparison: For simplicity, use a shallow comparison (===) when checking for memoized values.
  • No External Libraries: Do not use any external libraries for this challenge.
  • Code Clarity: Prioritize code readability and maintainability.

Notes

  • Consider using TypeScript's decorator syntax and type annotations extensively to ensure type safety.
  • Think about how to handle different types of properties and methods when creating the decorators.
  • The validate decorator should be applied to class properties.
  • The log decorator should be applied to class methods.
  • The memoize decorator should be applied to class methods.
  • Pay close attention to the return types of the decorated methods and ensure they are preserved.
  • The default value for validate should match the property's type.
Loading editor...
typescript