Overview
Decorators in TypeScript are a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use the form @expression
, where expression
must evaluate to a function that will be called at runtime with information about the decorated declaration. Implementing decorators for cross-cutting concerns like logging or caching adds a layer of abstraction, allowing these functionalities to be added in a non-intrusive way, thereby promoting cleaner and more maintainable code.
Key Concepts
- Decorator Factories: Functions that return a decorator function, allowing customization of the decorator behavior.
- Class Decorators: Applied to class constructors, they can observe, modify, or replace a class definition.
- Method Decorators: Applied to method declarations, they can observe, modify, or replace method definitions, and are useful for logging or caching.
Common Interview Questions
Basic Level
- What is a decorator in TypeScript?
- How do you create and apply a simple class decorator?
Intermediate Level
- How can method decorators be used to implement logging?
Advanced Level
- Discuss how to implement a caching mechanism using decorators for method results.
Detailed Answers
1. What is a decorator in TypeScript?
Answer: In TypeScript, a decorator is a design pattern that allows annotation and modification of classes and their members at design time. They are a form of metadata programming that can be used to wrap or encapsulate behaviors such as logging, caching, validation, and more. Decorators provide a cleaner syntax for the same operations that could be performed through other means, making the code more readable and maintainable.
Key Points:
- Decorators are a stage 2 proposal for JavaScript and are available in TypeScript.
- They can be applied to classes, methods, accessors, properties, and parameters.
- Decorators add metadata and behavior to the existing code without modifying the original class or method.
Example:
// Define a simple decorator
function logClass(target: Function) {
console.log(`${target.name} was decorated`);
}
// Apply the decorator to a class
@logClass
class MyClass {
constructor() {
console.log('MyClass instance created');
}
}
2. How do you create and apply a simple class decorator?
Answer: A class decorator is a function that takes the constructor of a class as its only argument and can be used to observe, modify, or replace a class definition. To create and apply a simple class decorator, you define a function that performs the desired operation and prefix a class definition with the decorator function name, preceded by an @
symbol.
Key Points:
- Class decorators receive the constructor function of the class as an argument.
- They can modify or replace the class definition.
- Decorators are applied at design time, but their effects occur at runtime.
Example:
// Class decorator that logs when a class is instantiated
function LogClass(target: any) {
// Save a reference to the original constructor
let original = target;
// A utility function to generate instances of a class
function construct(constructor, args) {
let c: any = function () {
return constructor.apply(this, args);
}
c.prototype = constructor.prototype;
return new c();
}
// The new constructor behavior
let f: any = function (...args) {
console.log(`New ${original.name} is created`);
return construct(original, args);
}
// Copy prototype so instanceof operator still works
f.prototype = original.prototype;
// Return new constructor (will override original)
return f;
}
@LogClass
class MyExampleClass {
constructor() { }
}
3. How can method decorators be used to implement logging?
Answer: A method decorator is applied to the property descriptor of the method and can be used to observe, modify, or replace a method definition. To implement logging, you can create a method decorator that wraps the original method and adds logging logic before and/or after the method invocation.
Key Points:
- Method decorators take three arguments: target, propertyKey, and descriptor.
- They can modify the behavior of the method.
- Useful for adding behaviors like logging, tracing, or validation.
Example:
function logMethod(target: any, key: string, descriptor: PropertyDescriptor) {
// save a reference to the original method
let originalMethod = descriptor.value;
// replace the original method with a wrapper
descriptor.value = function (...args: any[]) {
console.log(`Calling "${key}" with arguments: ${JSON.stringify(args)}`);
let result = originalMethod.apply(this, args);
console.log(`"${key}" returned: ${JSON.stringify(result)}`);
return result;
};
return descriptor;
}
class MyClass {
@logMethod
myMethod(arg: number): number {
return arg * 2;
}
}
4. Discuss how to implement a caching mechanism using decorators for method results.
Answer: Implementing a caching mechanism using method decorators involves intercepting method calls to cache the results based on the arguments passed. If the method is called again with the same arguments, the cached result is returned instead of executing the method again. This is particularly useful for expensive operations.
Key Points:
- Caching can significantly improve performance for methods with expensive operations.
- The decorator needs to maintain a cache, typically using a Map or object.
- Cache keys can be derived from the method arguments.
Example:
function cacheMethod(target: any, key: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
const cache = new Map<string, any>();
descriptor.value = function (...args: any[]) {
const cacheKey = JSON.stringify(args);
if (cache.has(cacheKey)) {
console.log(`Cache hit for key: ${cacheKey}`);
return cache.get(cacheKey);
} else {
console.log(`Cache miss for key: ${cacheKey}. Caching result.`);
const result = originalMethod.apply(this, args);
cache.set(cacheKey, result);
return result;
}
};
return descriptor;
}
class ExpensiveOperations {
@cacheMethod
calculateExpensiveValue(arg: number): number {
// Simulate an expensive operation
return arg * Math.random();
}
}
In this example, cacheMethod
is a decorator that wraps a method with caching logic, improving performance by avoiding repeated execution of expensive operations for the same input arguments.