Decorators are a powerful TypeScript feature that enables you to add metadata, modify behavior, or extend functionality of classes and their members. In this comprehensive guide, we’ll explore TypeScript decorator factories – a more flexible way to create and customize decorators.
Decorators have become increasingly important in modern TypeScript development, especially with frameworks like Angular that heavily utilize them. Let’s dive deep into decorator factories and understand how they can enhance your TypeScript applications.
Table of Contents
- What is a Decorator Factory?
- Creating Your First Decorator Factory
- Method Decorator Factories
- Class Decorator Factories
- Best Practices for Decorator Factories
- Practical Use Cases
- Common Pitfalls and Solutions
What is a Decorator Factory?
A decorator factory is a function that returns a decorator function. This extra level of indirection allows you to customize how the decorator behaves by passing parameters to it.
Here’s a basic example:
function log(prefix: string) { // This is the decorator factory
return function (target: any, propertyKey: string) { // This is the decorator
console.log(`${prefix} ${propertyKey}`);
};
}
Code language: JavaScript (javascript)
Creating Your First Decorator Factory
Let’s create a practical decorator factory that allows us to validate property values:
function minLength(length: number) {
return function (target: any, propertyKey: string) {
let value: string;
const getter = function () {
return value;
};
const setter = function (newVal: string) {
if (newVal.length < length) {
throw new Error(`${propertyKey} must be at least ${length} characters long`);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter
});
};
}
class User {
@minLength(5)
name: string;
constructor(name: string) {
this.name = name;
}
}
Code language: JavaScript (javascript)
Method Decorator Factories
Decorator factories can also be used to modify or enhance class methods:
function measureTime() {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`${propertyKey} took ${end - start}ms to execute`);
return result;
};
return descriptor;
};
}
class Calculator {
@measureTime()
complexCalculation(n: number): number {
// Simulate complex calculation
let result = 0;
for (let i = 0; i < n; i++) {
result += Math.sqrt(i);
}
return result;
}
}
Code language: JavaScript (javascript)
Class Decorator Factories
We can also create decorator factories for entire classes:
function singleton() {
return function <T extends { new(...args: any[]): {} }>(constructor: T) {
let instance: T;
return class extends constructor {
constructor(...args: any[]) {
if (instance) {
return instance;
}
super(...args);
instance = this;
}
};
};
}
@singleton()
class Database {
private constructor() {
console.log('Database instance created');
}
query(sql: string) {
console.log(`Executing query: ${sql}`);
}
}
Code language: JavaScript (javascript)
Best Practices for Decorator Factories
- Type Safety: Always provide proper type annotations for your decorator factory parameters:
function validate(validator: (value: any) => boolean) {
return function (target: any, propertyKey: string) {
// Implementation
};
}
Code language: PHP (php)
- Error Handling: Include appropriate error handling in your decorators:
function required() {
return function (target: any, propertyKey: string) {
let value: any;
const getter = function () {
if (value === undefined) {
throw new Error(`${propertyKey} is required but not set`);
}
return value;
};
const setter = function (newVal: any) {
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter
});
};
}
Code language: JavaScript (javascript)
- Documentation: Always document your decorator factories thoroughly:
/**
* Creates a decorator that logs method entry and exit
* @param prefix - The prefix to use in log messages
* @returns A method decorator
*/
function logMethod(prefix: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
// Implementation
};
}
Code language: PHP (php)
Practical Use Cases
API Method Decoration
function route(path: string) {
return function (target: any, propertyKey: string) {
Reflect.defineMetadata('path', path, target, propertyKey);
};
}
class API {
@route('/users')
getUsers() {
return ['user1', 'user2'];
}
@route('/posts')
getPosts() {
return ['post1', 'post2'];
}
}
Code language: JavaScript (javascript)
Validation Chains
function validate(...validators: ((value: any) => boolean)[]) {
return function (target: any, propertyKey: string) {
let value: any;
const getter = function () {
return value;
};
const setter = function (newVal: any) {
for (const validator of validators) {
if (!validator(newVal)) {
throw new Error(`Validation failed for ${propertyKey}`);
}
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter
});
};
}
Code language: PHP (php)
Common Pitfalls and Solutions
- Decorator Execution Order: Remember that decorators are executed in reverse order:
class Example {
@decorator1()
@decorator2()
method() {}
// decorator2 executes first, then decorator1
}
Code language: JavaScript (javascript)
- Maintaining the Prototype Chain: When creating class decorators, ensure you maintain the prototype chain:
function classDecorator() {
return function <T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
// Your modifications here
};
};
}
Code language: JavaScript (javascript)
Decorator factories are an essential part of TypeScript’s meta-programming capabilities. They provide a powerful way to add functionality to your code in a clean, reusable manner. As you continue working with TypeScript, you’ll find numerous opportunities to leverage decorator factories to create more maintainable and feature-rich applications.
For more insights into TypeScript features, check out our guide on TypeScript Type Assertions and TypeScript Access Modifiers.