Explore the experimental ES6+ decorators, their usage in JavaScript, and practical examples using TypeScript. Learn how decorators can enhance your code with annotations and modifications.
In the ever-evolving landscape of JavaScript, decorators represent a powerful yet experimental feature that allows developers to add annotations or modify classes and their members. As of 2023, decorators are a stage-2 proposal in the ECMAScript specification, meaning they are not yet standardized and may not be available in all JavaScript environments. However, with the help of transpilers like Babel or TypeScript, developers can leverage decorators to enhance their code with additional functionality and cleaner syntax.
Decorators provide a declarative way to apply behaviors to classes, methods, accessors, or properties. They are denoted by the @
symbol followed by a function. This function can be used to modify the behavior of the target it decorates. The concept of decorators is not new and has been popularized by languages like Python and frameworks such as Angular.
Since decorators are still a proposal, it’s important to note that their syntax and behavior might change in future ECMAScript versions. Developers should be cautious when using decorators in production code and ensure that their environment supports them. Tools like Babel and TypeScript offer support for decorators, making it easier to experiment with this feature.
To utilize decorators in your JavaScript projects, you need a transpiler that supports them. Both Babel and TypeScript offer this capability, with TypeScript providing a more seamless experience due to its built-in support for decorators.
To get started with decorators in TypeScript, you need to enable the experimentalDecorators
option in your tsconfig.json
file. Here’s a step-by-step guide:
Install TypeScript and Node.js:
npm install typescript ts-node @types/node
Configure tsconfig.json
:
{
"compilerOptions": {
"target": "ES6",
"experimentalDecorators": true
}
}
Create a Decorator:
Let’s create a simple logging decorator for a class method:
function log(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with`, args);
const result = originalMethod.apply(this, args);
console.log(`Result:`, result);
return result;
};
return descriptor;
}
Apply the Decorator:
Use the @log
decorator on a method within a class:
class Calculator {
@log
add(a: number, b: number) {
return a + b;
}
}
const calculator = new Calculator();
calculator.add(2, 3);
// Output:
// Calling add with [2, 3]
// Result: 5
Decorators can be applied to various elements within a class, each serving different purposes:
A class decorator is applied to the class constructor and can be used to modify or replace the class definition.
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return `Hello, ${this.greeting}`;
}
}
In this example, the @sealed
decorator prevents the addition of new properties to the Greeter
class and its prototype.
Method decorators are applied to methods within a class and can modify the method’s behavior.
function readonly(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.writable = false;
}
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@readonly
sayHello() {
return `Hello, my name is ${this.name}`;
}
}
Here, the @readonly
decorator makes the sayHello
method immutable, preventing it from being reassigned.
Accessor decorators are used to modify the accessors (getters and setters) of a property.
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
class Car {
private _speed: number = 0;
@configurable(false)
get speed(): number {
return this._speed;
}
set speed(value: number) {
this._speed = value;
}
}
The @configurable
decorator in this example makes the speed
getter non-configurable.
Property decorators are applied to class properties and can be used to observe or modify the property.
function logProperty(target: any, key: string) {
let _val = target[key];
const getter = () => {
console.log(`Get: ${key} => ${_val}`);
return _val;
};
const setter = (newVal) => {
console.log(`Set: ${key} => ${newVal}`);
_val = newVal;
};
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
}
class Product {
@logProperty
price: number;
constructor(price: number) {
this.price = price;
}
}
const product = new Product(100);
product.price = 200;
console.log(product.price);
In this example, the @logProperty
decorator logs access to the price
property.
To better understand the flow of decorators, consider the following diagram illustrating how a method decorator is applied:
flowchart LR Class --> Method Method --> Decorator[@log] Decorator --> ModifiedMethod ModifiedMethod --> Execution[Execute with Logging]
This flowchart demonstrates how the @log
decorator modifies a method to include logging functionality before and after its execution.
While decorators offer a powerful way to enhance your code, there are several best practices and considerations to keep in mind:
As decorators continue to evolve within the ECMAScript specification, they hold the potential to become a standardized feature in JavaScript. This would further solidify their role in modern JavaScript development, offering developers a robust tool for enhancing code with annotations and modifications.
For more information on decorators and their current status in the ECMAScript proposal process, you can refer to the TC39 Decorators Proposal.