A decorator is a function that allow us to customize an existing method. It allows for the modification of an object’s behavior — without changing its original code, but extending its functionality.
Decorators are commonly used in some JS frameworks/libraries like Angular and MobX, however they are still not a common design pattern developers used while writing their own code. With this article, I aim to document how we used decorators in my experience and hope to give you some ideas to utilise this design pattern in your own codebase.
Decorators are typically used with classes and prefixed with the @ symbol:
First, let's start with an Example
//value = the function being decorated
//kind = type of property, would be "method" here
//name = name of the function
function logged(value, { kind, name }) {
return function (...args) {
console.log(`starting ${name} with arguments ${args.join(", ")}`);
return value.call(this, ...args);
};
}
class Example {
@logged
method(arg) {
//Your method's code here
console.log("Inside method")
}
}
let ex = new Example()
ex.method(1);
/**
Outputs the following
[LOG]: "starting method with arguments 1"
[LOG]: "Inside method"
*/
The logged method here logs all the arguments being passed to a function. These decorators came in handy when we worked on a SDK, where the best way to debug was customer uploaded logs. Adding these decorators ensured code readability and reuse while giving our team an easy way to keep track of all the methods called by our users without much overhead. You can try running this code here
Now let's take a look at how you can include decorators in your codebase
Adding Decorators
Decorators are currently not a part of the standard JavaScript language. They are still being discussed in tc39 and have reached proposal stage 3. This means the spec has more or less stabilized and we can use them but they would be transplied before being run in the browser. This would be done via babel or tsc for most users
Here are the steps to enable decorators using babel:
- Install the
proposal-decorators
plugin
npm install --save-dev @babel/plugin-proposal-decorators
- Add the plugin to
.babelrc
{
"plugins": [
["@babel/plugin-proposal-decorators", { "version": "2023-05" }]
]
}
More details are available here
In TypeScript, standard decorators will work in principle out of the box. Just make sure:
- To upgrade to TypeScript to version 5 or higher
- Make sure the compiler targets "ES2015"or higher
- Disable or remove the experimentalDecoratorsflag
More examples:
Let's take a look at some other cases where decorators can be useful:
Abstract away repeated checks
Decorators could be very useful for abstracting repeated checks like Validation, Authentication and Authorization. In this example, we'll create a decorator to validate all the inputs to strictSum
and strictMulitply
methods
function Validator(value, {kind, name}) {
return function(...args) {
const validArgs = args.every(arg => Number.isInteger(arg));
if (!validArgs) {
throw new TypeError(`Argument passed to ${name} cannot be a non-integer`);
}
console.log(`All arguments passed to ${name} are valid`)
return value(...args);
}
}
class MathExample {
@Validator
strictSum(...args) {
//add all arguments passed
let result = args.reduce((acc, val) => acc + val, 0)
console.log(result)
return result
}
@Validator
strictMultiply(...args) {
//multiply all arguments passed
let result = args.reduce((acc, val) => acc * val, 1)
console.log(result)
return result
}
}
let math = new MathExample()
math.strictSum(1, 2, 3, 4, 5); //Works
math.strictMultiply(1, 2, 3, 4, "5"); //Throws an error
You can play around with this code here
The Validator
decorator allows us to be confident that the inputs are going to be numbers. Also, by encapsulating the validation logic in the decorator, we have made our methods much more readable and reused the validation code.
We could even write specific conditions for individual methods by checking the name
. For example, we can check the 0 isn't passed to a divide method.
In my team, we use such decorators to authenticate if a user was the moderator of a meeting and had the right to take actions like turn recording on, mute another participant and so on.
Measure time taken by a method
Decorators can be used for doing auxiliary actions like debugging without modifying the original code. Here we will create a custom @measureTime
decorator that logs the execution time of a method:
function measureTime(value, {kind, name}) {
const originalMethod = value;
return function (...args) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`Execution time for ${name}: ${end - start} milliseconds`);
return result;
};
}
class Timer {
@measureTime
heavyComputation() {
// Simulate a heavy computation
for (let i = 0; i < 1000000000; i++) {}
}
}
const timer = new Timer();
timer.heavyComputation(); // Logs execution time
You can test this code here
Add caching for computationally intensive methods
Another good use case for Decorators can be optimisation. For example, we can use decorators to cache or memoize the results of a computationally intensive method. Let's modify our MathExample class to see this in action
function memoize(value, {kind, name}) {
let cache = new Map();
return function(...args) {
let key = args.join()
if (cache.has(key)) {
console.log("returning from cache", cache.get(key))
return cache.get(key);
}
let result = value.apply(this, args);
cache.set(key, result);
return result;
};
}
class MathExample {
@memoize
multiply(...args) {
//multiply all arguments passed
let result = args.reduce((acc, val) => acc * val, 1)
console.log(result)
return result
}
}
let math = new MathExample()
math.multiply(1, 2, 3, 4, 5);
math.multiply(1, 2, 3, 4, 5);
math.multiply(1, 2, 3, 4, 5, 6);
math.multiply(1, 2, 3, 4, 5);
math.multiply(1, 2, 3, 4, 5, 6);
/**
* Output
[LOG]: "returning from cache", 120
[LOG]: "returning from cache", 120
[LOG]: "returning from cache", 720
*/
You can test this code here
Composition
Decorators have the powerful features of being composed and nested. It means we can apply multiple decorators to the same piece of code, and they’ll execute in a specific order. It helps in building complex and modular applications.
Lets combine @memoize
and @mesureTime
decorators to see composition in action
function memoize(value, {kind, name}) {
let cache = new Map();
return function(...args) {
let key = args.join()
if (cache.has(key)) {
console.log("returning from cache", cache.get(key))
return cache.get(key);
}
let result = value.apply(this, args);
cache.set(key, result);
return result;
};
}
function measureTime(value, {kind, name}) {
const originalMethod = value;
return function (...args) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`Execution time for ${name}: ${end - start} milliseconds`);
return result;
};
}
function wait(ms) {
let start = Date.now();
let now = start
while (now - start < ms) {
now = Date.now();
}
}
class MathExample {
@measureTime
@memoize
multiply(...args) {
//multiply all arguments passed
let result = args.reduce((acc, val) => acc * val, 1)
//wait for 1 seconds
wait(1000)
console.log(result)
return result
}
}
let math = new MathExample()
math.multiply(1,2,3,4,5);
math.multiply(1,2,3,4,5);
/**
* NOTE: The time might vary for each run, but note the difference between the memoized respons and the normal execution
[LOG]: "Execution time for multiply: 1000.5 milliseconds"
[LOG]: "returning from cache", 120
[LOG]: "Execution time for multiply: 0.19999980926513672 milliseconds"
*/
You can test this code here
There are many more such use-cases like:
Retry Mechanism. We can create decorators that automatically retry a method certain number of times in case of failures.
Event Handling. Decorators can trigger events before and after a method’s execution, enabling event-driven architectures.
Async cases. Decorators can also be used for asynchronous cases like debounce and throttle.
Class/property decorators. Decorators can also be applied on classes or individual properties. We've not covered these here, but they work in a similar manner. Please check the proposal for more details
Further reading
- tc39 decorators proposal
- Decorators documentation in TypeScript
- Babel plugin
- Sitepoint article - this article uses the older syntax for decorators, but explains the concepts well
I hope that this post demonstrates the flexibility of the decorator design pattern and encourages you to integrate it into your codebase. It's important to note that decorators may not be suitable for every use case. As a general guideline, avoid placing crucial business logic within decorators; instead, utilize them to encapsulate supplementary code.
Top comments (0)