Beyond Decorators: Alternative Techniques for Code Enhancement in TypeScript
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:
@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