Editor’s note: This post was reviewed 11 February 2022 to update code and include information about the latest breaking changes to Babel.
According to the Cambridge dictionary, to decorate something means “to add something to an object or place, especially in order to make it more attractive.”
Decorating in programming is simply wrapping one piece of code with another, thereby decorating it. A decorator (also known as a decorator function) can additionally refer to the design pattern that wraps a function with another function to extend its functionality.
This concept is possible in JavaScript because of first-class functions — JavaScript functions that are treated as first-class citizens.
The concept of decorators is not new in JavaScript because higher-order functions are a form of function decorators.
Let’s elaborate on this in the next section, or jump to your section of interest below:
Function decorators are functions. They take a function as an argument and return a new function that enhances the function argument without modifying it.
In JavaScript, higher-order functions take a first-class function as an argument and/or return other functions.
Consider the code below:
const logger = (message) => console.log(message) function loggerDecorator (logger) { return function (message) { logger.call(this, message) console.log("message logged at:", new Date().toLocaleString()) } } const decoratedLogger = loggerDecorator(logger);
We have decorated the logger
function by using the loggerDecorator
function. The returned function — now stored in the decoratedLogger
variable — does not modify the logger
function. Instead, the returned function decorates it with the ability to print the time a message is logged.
Consider the code below:
logger("Lawrence logged in: logger") // returns Lawrence logged in: logger decoratedLogger("Lawrence logged in: decoratedLogger") // returns: // Lawrence logged in: decoratedLogger // message logged at: 6/20/2021, 9:18:39 PM
We see that when the logger
function is called, it logs the message to the console. But when the decoratedLogger
function is called, it logs both the message and current time to the console.
Below is another sensible example of a function decorator:
//ordinary multiply function let Multiply = (...args) => { return args.reduce((a, b) => a * b) } // validated integers const Validator = (fn) => { return function(...args) { const validArgs = args.every(arg => Number.isInteger(arg)); if (!validArgs) { throw new TypeError('Argument cannot be a non-integer'); } return fn(...args); } } //decorated multiply function that only multiplies integers MultiplyValidArgs = Validator(Multiply); MultiplyValidArgs(6, 8, 2, 10);
In our code above, we have an ordinary Multiply
function that gives us the product of all its arguments. However, with our Validator
function — which is a decorator — we extend the functionality of our Multiply
function to validate its input and multiply only integers.
In JavaScript, function decorators exist since the language supports higher-order functions. The pattern used in function decorators cannot easily be used on JavaScript classes. Hence, the TC39 class decorator proposal. You can learn more about the TC39 process here.
The TC39 class decorator proposal aims to solve this problem:
function log(fn) { return function() { console.log("Logged at: " + new Date().toLocaleString()); return fn(); } } class Person { constructor(name, age, job) { this.name = name; this.age = age; this.job = job; } getBio() { return `${this.name} is a ${this.age} years old ${this.job}`; } } // creates a new person let man = new Person("Lawrence", 20, "developer"); // decorates the getBio method let decoratedGetBio = log(man.getBio); decoratedGetBio(); // TypeError: Cannot read property 'name' of undefined at getBio
We tried to decorate the getBio
method using the function decorator technique, but it does not work. We get a TypeError
because when the getBio
method is called inside the log
function, the this
variable refers the inner function to the global object.
We can work around this by binding the this
variable to the man
instance of the Person
class as seen below:
// decorates the getBio method let decoratedGetBio = log(man.getBio.bind(man)); decoratedGetBio(); // returns // Logged at: 6/22/2021, 11:56:57 AM // Lawrence is a 20 years old developer
Although this works, it requires a bit of a hack and a good understanding of the JavaScript this
variable. So there is a need for a cleaner and easier-to-understand method of using decorators with classes.
Class decorators — or strictly decorators — are a proposal for extending JavaScript classes. TC39 is currently a stage 2 proposal, meaning they are expected to be developed and eventually included in the language.
However, with the introduction of ES2015+, and as transpilation has become commonplace, we can use this feature with the help of tools such as Babel by using the @babel/plugin-proposal-decorators.
Also, it is important to note that the stage 2 decorator proposal has a new and stable implementation to handle multiple constraints and desires. And the new semantics are different from the old proposal — decorators already written for Babel legacy or TypeScript will not work with the new proposal.
According to the Babel-rewrite documentation, to support this new implementation, we need to pass the "version": "2021-12"
option to the @babel/plugin-proposal-decorators
as seen below:
{ "plugins": [ ["@babel/plugin-proposal-decorators", { "version": "2021-12" }] ] }
Note: for development purposes you can use the new decorators playground.
Decorators use a special syntax whereby they are prefixed with an @
symbol and placed immediately above the code being decorated, as seen below:
@log class ExampleClass { doSomething() { // } }
Also, class decorators come after export
and default
, and it is possible to decorate both a class declaration and class expression.
When a decorator is called, it receives two arguments: value
and context
. The value
argument refers to the value being decorated but it is undefined
if it is a class field and the context refers to an object that contains metadata about the value being decorated.
In the new implementation, decorators support classes and public, private, and static class members such as methods, accessors, and class fields.
A simple implementation is shown in the code below:
function decorator (value, context) { console.log("decorated value is:", value); console.log("context is: ", context); } @decorator class C { @decorator // decorates a class field p = 5; @decorator // decorates a method m() {} @decorator // decorates a getter get x() {} @decorator // decorates a setter set x(v) {} }
Let’s learn more about the different types of decorators with the detailed examples given below:
A class member decorator is a binary function applied to members of a class.
The first argument, value
, refers to the member property of the class we are decorating. This makes possible a pattern where we can optionally return a new method or replace the decorated function.
If we return a new method, it will replace the original on the prototype, but if it is a static method, it will replace it on the class itself. However, if we return any other type of value, an error will be thrown.
Let’s learn about this by rewriting our log
decorator:
function log(value, {kind, name}) { if (kind === "method") { return function(...args) { console.log("Logged at: " + new Date().toLocaleString()); try { const result = value.apply(this, args); return result; } catch (e) { console.log(`Error: ${e}`); throw e; } } } } class Person { constructor(name, age, job) { this.name = name; this.age = age; this.job = job; } @log getBio() { return `${this.name} is a ${this.age} years old ${this.job}`; } } // creates a new person let man = new Person("Lawrence", 20, "developer"); man.getBio()
In the code above, we have successfully refactored our log
decorator — from function decorator pattern to member class decorator.
We simply accessed the member class property — in this case, the getBio
method — with the descriptor value
, and replaced it with a new function.
This is cleaner and can be more easily reused than plain higher-order functions.
These decorators are applied to the whole class, enabling us to decorate the class.
The class decorator receives a class as the first argument. And it can optionally return a new class or replace the decorated class, but it throws an error if a nonconstructable value is returned.
Consider the code below:
function log(value, { kind, name }) { if (kind === "class") { const newClass = class extends value { constructor(...args) { super(...args); console.log(`constructing a class with arguments: ${args.join(", ")}`); } } console.log(`An instance of the ${name} ${kind} has been created`) return newClass; } } @log class Person { constructor(name, profession) { } } const lawrence = new Person('Lawrence Eagles', "Developer");
In our small, contrived example, the log
decorator receives the class as the first argument and logs information of an instance whenever a new class is created.
Decorators enable us to write cleaner code by providing an efficient and understandable way of wrapping one piece of code with another. It also provides a clean syntax for applying this wrapper.
This syntax makes our code less distracting because it separates the feature-enhancing code away from the core function. And it enables us to add new features without increasing our code complexity.
Additionally, decorators help us extend the same functionality to several functions and classes, thereby enabling us to write code that is easier to debug and maintain.
While decorators already exist in JavaScript as higher-order functions, it is difficult or even impossible to implement this technique in classes. Hence, the special syntax TC39 offers is for easy usage with classes.
Although decorators are a stage 2 proposal, they are already popular in the JavaScript world — thanks to Angular and TypeScript.
From this article, we can see that they foster code reusability, thereby keeping our code DRY.
The new implementation enables us to decorate a class and everything within the class including the new class auto-accessors.
As we wait for decorators to be officially available in JavaScript, you can start using them by using Babel. And I believe you have learned enough in this article to give decorators a try in your next project.
Debugging code is always a tedious task. But the more you understand your errors, the easier it is to fix them.
LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
5 Replies to "Understanding JavaScript decorators"
Error in code there
“Let’s learn about this by rewriting our log decorator:
function log(target, name, descriptor) {
if (typeof original === ‘function’) { <—– what is "original" ?
The @log class method example is completely wrong and needs to be updated. The log function receives 3 arguments: the target object, the name of the target class member and a descriptor, an object containing the implementation of the class member.
Hello A, it is important to note that the decorator API has evolved. You seem to refer to the stage 2 API. Below is the updated shape of the current sage 3 API in TypeScript:
type Decorator = (value: Input, context: {
kind: string;
name: string | symbol;
access: {
get?(): unknown;
set?(value: unknown): void;
};
private?: boolean;
static?: boolean;
addInitializer?(initializer: () => void): void;
}) => Output | void;
Checkout https://github.com/tc39/proposal-decorators for more.
Hi Yome, this article has been updated to use the latest JavaScript API and the code works well.
However, thanks for your comment.
Decorators are at stage 3 now as of December 2022: https://github.com/tc39/proposal-decorators