In 2015, ECMAScript 6 was introduced – a significant release of the JavaScript language. This release introduced many new features, such as const/let, arrow functions, classes, etc. Most of these features were aimed at eliminating JavaScript's quirks. For this reason, all these features were labeled as "Harmony." Some sources say that the entire ECMAScript 6 is called "ECMAScript Harmony." In addition to these features, the "Harmony" label highlights other features expected to become part of the specification soon. Decorators are one of such anticipated features.

Nearly 10 years have passed since the first mentions of decorators. The decorators’ specification has been rewritten several times almost from scratch but they have not become part of the specification yet. As JavaScript has long extended beyond just browser-based applications, authors of specifications must consider a wide range of platforms where JavaScript can be executed. This is precisely why progressing to stage 3 for this proposal has taken so much time.

Something Completely New?

First of all, let's clarify what decorators are in the programming world.

Decorator is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors.

© https://refactoring.guru/design-patterns/decorator

The key point here is that a decorator is a design pattern. This means that typically it can be implemented in any programming language. If you have even a basic familiarity with JavaScript, chances are you have already used this pattern without even realizing it.

Sound interesting? Then try to guess what the most popular decorator in the world is... Meet the most famous decorator in the world, the higher-order function – debounce.

Debounce

Before we delve into the details of the debounce function, let's remind ourselves what higher-order functions are. Higher-order functions are functions that take one or more functions as arguments or return a function as their result. The debounce function is a prominent example of a higher-order function and at the same time the most popular decorator for JS developers.

The higher-order function debounce delays the invocation of another function until a certain amount of time has passed since the last invocation, without changing its behavior. The most common use case is to prevent sending multiple requests to the server when a user is inputting values into a search bar, such as loading autocomplete suggestions. Instead, it waits until the user has finished or paused input and only then sends the request to the server.

On most resources for learning JavaScript language in the section about timeouts, you will find exercises that involve writing this function. The simplest implementation looks like this:

const debounce = (fn, delay) => {
  let lastTimeout = null

  return (...args) => {
    clearInterval(lastTimeout)

    lastTimeout = setTimeout(() => fn.call(null, ...args), delay)
  }
}

Using this function may look like the following:

class SearchForm {
  constructor() {
    this.handleUserInput = debounce(this.handleUserInput, 300)
  }

  handleUserInput(evt) {
    console.log(evt.target.value)
  }
}

When using a special syntax for decorators which we will discuss in the next section, the implementation of the same behavior will look like this:

class SearchForm {
  @debounce(300)
  handleUserInput(evt) {
    console.log(evt.target.value)
  }
}

All the boilerplate code is gone, leaving only the essentials. Looks nice and clean, doesn't it?

Higher-Order Component (HOC)

The next example will come from the React world. Although the use of Higher-Order Components (HOC) is becoming less common in applications built with this library, HOCs still serve as a good and well-known example of decorator usage.

Let's take a look at an example of the withModal HOC:

const withModal = (Component) => {
  return (props) => {
    const [isOpen, setIsOpen] = useState(false)

    const handleModalVisibilityToggle = () => setIsOpen(!isOpen)

    return (
      <Component
        {...props}
        isOpen={isOpen}
        onModalVisibilityToggle={handleModalVisibilityToggle}
      />
    )
  }
}

And now, let's see how it can be used:

const AuthPopup = ({ onModalVisibilityToggle }) => {
  // Component
}

const WrappedAuthPopup = withModal(AuthPopup)

export { WrappedAuthPopup as AuthPopup }

Here is what using the HOC with the special decorator syntax would look like:

@withModal()
const AuthPopup = ({ onModalVisibilityToggle }) => {
  // Component
}

export { AuthPopup }

Important Note: Function decorators are not a part of the current proposal. However, they are on the list of things that could be considered for the future development of the decorator’s specification.

Once again, all the boilerplate code is gone, leaving only what truly matters. Perhaps some of the readers did not see anything special in this. In the example above, only one decorator was used.

Let's take a look at such an example:

const AuthPopup = ({
  onSubmit,
  onFocusTrapInit,
  onModalVisibilityToggle,
}) => {
  // Component
}

const WrappedAuthPopup = withForm(
  withFocusTrap(
    withModal(AuthPopup)
  ), {
  mode: 'submit',
})

export { WrappedAuthPopup as AuthPopup }

See that hard-to-read nesting?

How much time did it take you to understand what is happening in the code?

Now, let's take a look at the same example but with the use of decorator syntax:

@withForm({ mode: 'submit' })
@withFocusTrap()  
@withModal()
const AuthPopup = ({
  onSubmit,
  onFocusTrapInit,
  onModalVisibilityToggle,
}) => {
  // Component
}

export { AuthPopup }

Would you not agree that the code that goes from top to bottom is much more readable than the previous example with nested function calls?

The higher-order function debounce and the higher-order component withModal are just a few examples of how the decorator pattern is applied in everyday life. This pattern can be found in many frameworks and libraries that we use regularly, although many of us may not pay attention to it. Try analyzing the project you are working on and look for places where the decorator pattern is applied. You will likely discover more than one such example.

JavaScript Implementations

Before we delve into the decorator proposal itself and its implementation, I would like us to take a look at this image:

With this image, I would like to remind you of the primary purpose for which JavaScript language was originally created. I am not one of those people who like to complain, saying, "Oh, JavaScript is only good for highlighting form fields." Typically, I refer to such individuals as “dinosaurs”.

JavaScript primarily focuses on the end user for whom we write code. This is a crucial point to understand because every time new things are introduced in JavaScript language such as classes with implementations differing from what is found in other programming languages, the same complainers come and start lamenting that things are not done in a user-friendly manner. Quite the opposite, in JavaScript everything is designed with end users in mind, which is something that no other programming language can boast about.

Today, JavaScript is not just a browser language. It can be run in various environments, including on the server. The TC39 committee responsible for introducing new features to the language, faces the challenging task of meeting the needs of all platforms, frameworks, and libraries. However, the primary focus remains on end users in the browser.

History of Decorators

To delve deeper into the history of this proposal, let's review a list of key events.

Just Syntactic Sugar or Not?

After all the explanations and examples, you might have a question: "So, are decorators in JavaScript just higher-order functions with special syntax, and that is it?”.

It is not all that simple. In addition to what was mentioned earlier regarding how JavaScript primarily focuses on end-users, it is also worth adding that JS-engines always try to use the new syntax as a reference point to at least attempt to make your JavaScript faster.

import { groupBy } from 'npm:[email protected]'

const getGroupedOffersByCity = (offers) => {
  return groupBy(offers, (it) => it.city.name)
}

// OR ?

const getGroupedOffersByCity = (offers) => {
  return Object.groupBy(offers, (it) => it.city.name)
}

It may seem like there is no difference, but there are distinctions for the engine. Only in the second case, when native functions are used, can the engine attempt optimization.

Describing how optimizations work in JavaScript engines would require a separate article. Do not hesitate to explore browser source code or search for articles to gain a better understanding of this topic.

It is also important to remember that there are many JavaScript engines, and they all perform optimizations differently. However, if you assist the engine by using native syntax, your application code will generally run faster in most cases.

Possible Extensions

The new syntax in the specification also opens the door for additional features in the future. As an analogy, consider constructor functions and classes. When private fields were introduced in the specification, they were introduced as a feature for classes. For those who staunchly denied the usefulness of classes and claimed that constructor functions were equivalent, private fields became another reason to move away from constructor functions in favor of classes. Such features are likely to continue evolving.

While we can currently achieve many of the same effects as decorators using higher-order functions in many cases, they still do not cover all the potential functionality that will be added to the decorators specification in the future.

The "possible extensions" file in the decorators specification repository provides insights into how the decorators specification may evolve in the future. Some of the points were listed in the first stages but are not present in the current standard, such as parameter decorators. However, there are also entirely new concepts mentioned, like const/let decorators or block decorators. These potential extensions illustrate the ongoing development and expansion of the decorator functionality in JavaScript.

Indeed, numerous proposals and extensions are being considered to enhance the decorators specification further. Some of these proposals like the Decorator Metadata, are already under consideration even though the core decorator specification has not yet been standardized. This underscores the idea that decorators have a promising future in the specification and we can hope to see them become a part of the standard in the near future.

Conclusion

The lengthy consideration of the decorators proposal over 10 years may indeed seem like an extended period. It is true that the early adoption of decorators by leading frameworks and libraries played a role in uncovering the shortcomings of the initial implementation. However, this early adoption also served as an invaluable learning experience, highlighting the importance of harmonizing with web platforms and developing a solution that aligns with both platforms and the developer community, while preserving the essence of decorators. The time spent refining the proposal has ultimately contributed to making it a more robust and well-considered addition to the JavaScript language.

Indeed, decorators will bring significant changes to how we write applications today. Perhaps not immediately, as the current specification primarily focuses on classes, but with all the additions and ongoing work JavaScript code in many applications will soon look different. We are now closer than ever to the moment when we can finally see those ones that are real decorators in the specification. It is an exciting development that promises to enhance the expressiveness and functionality of JavaScript applications.

Also published here.