JavaScript decorators are a powerful feature that allows developers to modify the behaviour of classes and their members. Decorators provide a clean and readable way to add annotations or meta-programming syntax for class declarations and members. This article delves into the magic of JavaScript decorators, explaining how they work and how they can be used to enhance your classes and methods.
What are JavaScript Decorators?
Decorators are a stage 2 proposal for JavaScript, which means they are not yet a part of the ECMAScript standard but are widely used in modern JavaScript frameworks like Angular and libraries like TypeScript. A decorator is a special kind of declaration that can be attached to a class, method, accessor, property, or parameter. Decorators can modify the behavior of the decorated element in a declarative manner.
Basic Syntax
Decorators are denoted by the @
symbol followed by an expression. They can be applied to classes, methods, accessors, properties, and parameters.
function MyDecorator(target) {
// Do something with the target
}
@MyDecorator
class MyClass {
// ...
}
Applying Decorators
Decorators can be applied to various elements of a class:
- Class Decorators: Applied to the entire class.
- Method Decorators: Applied to a method.
- Accessor Decorators: Applied to getters and setters.
- Property Decorators: Applied to a class property.
- Parameter Decorators: Applied to a method parameter.
Example of a Class Decorator
A class decorator is a function that takes a single parameter: the constructor of the class.
function sealed(constructor) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class MyClass {
constructor(name) {
this.name = name;
}
}
In this example, the sealed
decorator seals the constructor and its prototype, preventing any extensions to the class.
Enhancing Methods with Decorators
Method decorators are used to modify the behavior of methods. They take three parameters: the target (either the constructor function for a static method or the prototype of the class for an instance method), the name of the method, and the property descriptor.
Example of a Method Decorator
function log(target, key, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args) {
console.log(`Calling ${key} with arguments`, args);
const result = originalMethod.apply(this, args);
console.log(`Result of ${key}:`, result);
return result;
};
return descriptor;
}
class Calculator {
@log
add(a, b) {
return a + b;
}
}
const calculator = new Calculator();
calculator.add(2, 3); // Console: Calling add with arguments [2, 3], Result of add: 5
In this example, the log
decorator wraps the original add
method, logging the method name and arguments before calling the method, and logging the result afterward.
Property Decorators
Property decorators are used to observe and modify the behavior of properties. They receive two parameters: the target (either the constructor function for a static member or the prototype of the class for an instance member) and the name of the property.
Example of a Property Decorator
function readonly(target, key, descriptor) {
descriptor.writable = false;
return descriptor;
}
class Person {
@readonly
name = 'John Doe';
}
const person = new Person();
person.name = 'Jane Doe'; // TypeError: Cannot assign to read-only property 'name'
In this example, the readonly
decorator sets the writable
property of the descriptor to false
, making the name
property read-only.
Accessor Decorators
Accessor decorators are applied to properties and methods to control access to class members.
Example of an Accessor Decorator
function configurable(value) {
return function (target, key, descriptor) {
descriptor.configurable = value;
return descriptor;
};
}
class Car {
constructor(make, model) {
this.make = make;
this.model = model;
}
@configurable(false)
get description() {
return `${this.make} ${this.model}`;
}
}
const myCar = new Car('Toyota', 'Camry');
console.log(myCar.description); // "Toyota Camry"
In this example, the configurable
decorator modifies the configurable
attribute of the description
property.
Parameter Decorators
Parameter decorators are used to annotate or modify function parameters.
Example of a Parameter Decorator
function required(target, key, index) {
console.log(`Parameter at position ${index} in ${key} is required`);
}
class User {
greet(@required name) {
return `Hello, ${name}`;
}
}
const user = new User();
user.greet('Alice'); // Console: Parameter at position 0 in greet is required
In this example, the required
decorator logs a message indicating that a parameter is required.
Best Practices and Use Cases
- Code Reusability: Decorators allow you to define reusable functionalities that can be easily applied to different classes or methods.
- Separation of Concerns: Decorators help separate core logic from auxiliary functionalities like logging, validation, or authorization.
- Readability: Decorators make the code more readable by providing a clear and concise way to apply behaviors.
Common Use Cases
- Logging: Automatically log method calls and results.
- Validation: Validate method parameters or class properties.
- Authorization: Check user permissions before executing a method.
- Caching: Implement caching mechanisms for expensive operations.
- Error Handling: Automatically catch and handle errors in methods.
Advanced Usage
Composing Multiple Decorators
You can apply multiple decorators to a single element. They are evaluated in reverse order of their appearance.
function first(target, key, descriptor) {
console.log('first');
return descriptor;
}
function second(target, key, descriptor) {
console.log('second');
return descriptor;
}
class Example {
@first
@second
method() {
console.log('method');
}
}
const example = new Example();
example.method(); // Console: second, first, method
Using Decorators with Metadata
Decorators can also interact with metadata, providing a way to store and retrieve metadata about classes and methods.
import 'reflect-metadata';
function metadata(key, value) {
return function (target, propertyKey) {
Reflect.defineMetadata(key, value, target, propertyKey);
};
}
class Example {
@metadata('role', 'admin')
method() {}
}
const role = Reflect.getMetadata('role', Example.prototype, 'method');
console.log(role); // "admin"
In this example, the metadata
decorator adds metadata to the method
function.
JavaScript decorators are a powerful tool for enhancing the behavior of classes and methods in a clean, readable, and reusable way. While still a proposal, decorators are widely used in modern JavaScript development, especially in frameworks like Angular and libraries like TypeScript. By mastering decorators, you can write more maintainable and scalable code. Remember to use decorators to improve code reusability, separation of concerns, and readability.
To learn more about decorators, refer to the TC39 Decorators Proposal and explore frameworks and libraries that implement this feature.
Happy coding!
Top comments (2)
Hello fellow decorator user, can you provide any feedback on this decorator issue? github.com/tc39/proposal-decorator...
Hi! I see, that decorator-proposal, at the moment on stage 3 not stage 2
github.com/tc39/proposal-decorators