JavaScript decorators provide a powerful way to modify classes and their members at design time. Let's explore how they work and how to use them effectively in your applications. 🎨
Understanding Decorators 🔍
Decorators are special functions that can modify classes and their members. They use the @
syntax and can be applied to:
- Classes
- Methods
- Properties
- Accessors
- Parameters
Basic Decorator Syntax
Here's how to create and use a simple decorator:
function log(target, name, descriptor) {
// Store the original method
const original = descriptor.value;
// Replace with new functionality
descriptor.value = function(...args) {
console.log(`Calling ${name} with:`, args);
return original.apply(this, args);
};
return descriptor;
}
class Calculator {
@log
add(a, b) {
return a + b;
}
}
Class Decorators
Class decorators can modify or replace class definitions:
function singleton(target) {
// Store the original constructor
const originalConstructor = target;
// Create a new constructor function
function newConstructor(...args) {
if (!newConstructor.instance) {
newConstructor.instance = new originalConstructor(...args);
}
return newConstructor.instance;
}
// Copy prototype
newConstructor.prototype = originalConstructor.prototype;
return newConstructor;
}
@singleton
class ConfigManager {
constructor() {
this.config = {};
}
setConfig(key, value) {
this.config[key] = value;
}
}
Method Decorators
Method decorators can modify method behavior:
function debounce(delay) {
return function(target, name, descriptor) {
const original = descriptor.value;
let timeout;
descriptor.value = function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => {
original.apply(this, args);
}, delay);
};
return descriptor;
};
}
class SearchComponent {
@debounce(300)
search(query) {
// Perform search operation
console.log(`Searching for: ${query}`);
}
}
Property Decorators
Property decorators can modify how properties behave:
function readonly(target, name, descriptor) {
descriptor.writable = false;
return descriptor;
}
class User {
@readonly
id = Math.random();
}
Parameter Decorators
Parameter decorators can be used for dependency injection or validation:
function validate(target, key, parameterIndex) {
const validateParams = target[key].validateParams || [];
validateParams[parameterIndex] = true;
target[key].validateParams = validateParams;
}
class UserService {
createUser(@validate username, @validate email) {
// Implementation
}
}
Real-World Use Cases 🌟
Authentication Decorator
function requireAuth(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
if (!this.isAuthenticated()) {
throw new Error('Authentication required');
}
return original.apply(this, args);
};
return descriptor;
}
class UserDashboard {
@requireAuth
viewProfile() {
return this.userData;
}
}
Performance Monitoring
function measure(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = async function(...args) {
const start = performance.now();
const result = await original.apply(this, args);
const end = performance.now();
console.log(`${name} took ${end - start}ms to execute`);
return result;
};
return descriptor;
}
class DataProcessor {
@measure
async processLargeDataset(data) {
// Processing logic
}
}
Advanced Patterns 🚀
Decorator Composition
Multiple decorators can be combined:
function log(target, name, descriptor) {
// Logging implementation
}
function validate(target, name, descriptor) {
// Validation implementation
}
class API {
@log
@validate
@measure
async fetchData() {
// Fetch implementation
}
}
Factory Decorators
Create decorators with configuration:
function timeout(ms) {
return function(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = async function(...args) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), ms);
});
return Promise.race([
original.apply(this, args),
timeoutPromise
]);
};
return descriptor;
};
}
class API {
@timeout(5000)
async fetchData() {
// Fetch implementation
}
}
Best Practices 📝
- Keep decorators focused and single-purpose
- Use meaningful names that describe the decorator's function
- Document decorator behavior and requirements
- Consider performance implications
- Handle errors gracefully
- Test decorated code thoroughly
TypeScript Integration
TypeScript provides excellent support for decorators:
function propertyType(type: string) {
return function(target: any, propertyKey: string) {
let value = target[propertyKey];
const getter = () => value;
const setter = (newVal: any) => {
if (typeof newVal !== type) {
throw new Error(`${propertyKey} must be a ${type}`);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
};
}
class User {
@propertyType('string')
name: string;
}
Debugging Decorated Code 🐛
When debugging code that uses decorators:
- Use source maps
- Inspect the descriptor object
- Add logging to decorator functions
- Use browser developer tools
Future of Decorators
Decorators are evolving with the JavaScript language:
- Stage 3 decorator proposal
- New capabilities and syntax
- Better performance optimizations
- Enhanced type system integration
Additional Resources
JavaScript decorators are a powerful tool for metaprogramming, enabling cleaner, more maintainable code through separation of concerns. As the specification matures, we can expect even more powerful capabilities in the future.