JavaScript decorators are a powerful feature that allows you to modify the behavior of classes and their members in a declarative manner. Initially popularized in other programming languages like Python and Java, decorators provide a way to add functionality to methods, properties, and entire classes without altering their original code. This article will guide you through the process of implementing JavaScript decorators, explaining the concepts with clear examples and detailing their results.
What are Decorators?
Decorators are functions that take a target (the element to be decorated) and optionally, some metadata (like property descriptors). They return a new target or modify the existing one. Decorators can be applied to classes, methods, accessors, properties, and parameters.
Basic Syntax
The basic syntax of a decorator is straightforward. Here’s a simple example of a decorator function:
function myDecorator(target) {
// Modify the target in some way
target.decorated = true;
}
Applying Decorators to a Class
To apply a decorator to a class, you use the @ symbol before the class definition. Here’s an example:
function addTimestamp(target) {
target.prototype.timestamp = new Date();
}
@addTimestamp
class MyClass {}
const instance = new MyClass();
console.log(instance.timestamp); // Outputs the current date and time
In this example, the addTimestamp decorator adds a timestamp property to the MyClass class. When you create an instance of MyClass, it includes the timestamp property initialized to the current date and time.
Method Decorators
Method decorators can modify the behavior of class methods. Here’s how you can create and apply a method decorator:
function logMethod(target, propertyKey, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args) {
console.log(`Calling ${propertyKey} with arguments:`, args);
return originalMethod.apply(this, args);
};
return descriptor;
}
class MyClass {
@logMethod
greet(name) {
return `Hello, ${name}!`;
}
}
const instance = new MyClass();
console.log(instance.greet('World'));
// Output:
// Calling greet with arguments: [ 'World' ]
// Hello, World!
In this example, the logMethod decorator logs the method name and its arguments whenever the method is called, and then calls the original method.
Accessor Decorators
Accessor decorators can be used to modify the behavior of getters and setters. Here’s an example:
function capitalize(target, propertyKey, descriptor) {
const originalGetter = descriptor.get;
descriptor.get = function () {
const result = originalGetter.call(this);
return result.toUpperCase();
};
return descriptor;
}
class Person {
constructor(name) {
this._name = name;
}
@capitalize
get name() {
return this._name;
}
}
const person = new Person('john');
console.log(person.name); // Outputs: JOHN
In this case, the capitalize decorator modifies the getter for the name property to return the name in uppercase.
Property Decorators
Property decorators can be used to add metadata to properties. However, they don’t directly modify property values. Here’s an example:
function readOnly(target, propertyKey) {
Object.defineProperty(target, propertyKey, {
writable: false
});
}
class Car {
@readOnly
make = 'Toyota';
}
const myCar = new Car();
console.log(myCar.make); // Outputs: Toyota
myCar.make = 'Honda'; // This will fail silently or throw an error in strict mode
console.log(myCar.make); // Still outputs: Toyot
In this example, the readOnly decorator makes the make property non-writable, so any attempts to change its value will not succeed.
Parameter Decorators
Parameter decorators can be used to add metadata to method parameters. Here’s an example:
function logParameter(target, propertyKey, parameterIndex) {
const originalMethod = target[propertyKey];
target[propertyKey] = function (...args) {
console.log(`Parameter at index ${parameterIndex} in method ${propertyKey} is:`, args[parameterIndex]);
return originalMethod.apply(this, args);
};
}
class User {
greet(@logParameter name) {
return `Hello, ${name}!`;
}
}
const user = new User();
console.log(user.greet('Alice'));
// Output:
// Parameter at index 0 in method greet is: Alice
// Hello, Alice!
In this example, the logParameter decorator logs the value of a specific parameter whenever the method is called.
JavaScript decorators provide a powerful and flexible way to enhance the functionality of classes and their members. By using decorators, you can keep your code clean and modular, adding features like logging, validation, or transformation in a declarative manner. The examples provided in this article demonstrate the various types of decorators and how they can be applied to different parts of your code. As decorators become more widely supported and standardized in JavaScript, they will undoubtedly become an essential tool in every developer’s toolkit.
How to Implement JavaScript Decorators was originally published in CarlosRojasDev on Medium, where people are continuing the conversation by highlighting and responding to this story.