ECMAScript Decorators. The Ones That are Real.

Content
Changelog
  • Added info about esbuild 0.21.3, support for Decorator Metadata

  • Added info about esbuild 0.21, support for ECMAScript Decorators

  • Added info about the Function Decorators Stage 1 proposal

  • Added info about Deno 1.40, support for ECMAScript Decorators

  • Added info about MobX 6.11, support for ECMAScript Decorators

  • Added info about Lit 3.0, support for ECMAScript Decorators

  • Added info about Babel 7.23.0, support for Decorator Metadata

  • Initial release

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 decorators 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 often 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 decorators proposal itself and its implementation, I would like us to take a look at this image:

Screenshot of an old browser with an HTML form.
Screenshot of an old browser with an HTML form.

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.

  • 2014-04 – Stage 0. Decorators were proposed by Yehuda Katz and they were initially intended to become a part of the ECMAScript 7.

    type Decorator = (
      target: DecoratedClass,
      propertyKey: string,
      descriptor: PropertyDescriptor,
    ) => PropertyDescriptor | void
    
    function debounce(delay: number): PropertyDescriptor {
      return (target, propertyKey, descriptor) => {
        let lastTimeout: number
        const method = descriptor.value
    
        descriptor.value = (...args: unknown[]) => {
          clearInterval(lastTimeout)
    
          lastTimeout = setTimeout(() => method.call(null, ...args), delay)
        }
    
        return descriptor
      }
    }

    Already at this stage, you can see one of the reasons why the decorators API underwent such significant changes later on. The first argument of the decorator was an entire class, even if you were decorating only one of its members. Moreover, it was assumed that developers could mutate this class. JavaScript engines always strive to optimize as much as possible, and in this case, the developer's call to mutate the entire class undermined a significant number of optimizations provided by the engine. Later, we will see that this was indeed a major reason why the decorators API was rewritten multiple times, almost from scratch.

  • 2015-03 – Stage 1. Without significant changes, the proposal advanced to stage 2. However, an event occurred that significantly influenced the further development of this proposal: TypeScript 1.5 was released, which supported decorators. Despite decorators being marked as experimental (--experimentalDecorators), projects like Angular and MobX actively started using them. Furthermore, the overall workflow for these projects assumed the use of decorators exclusively. Due to the popularity of these projects, many developers mistakenly believed that decorators were already a part of the official JS standard.

    This created additional challenges for the TC39 committee because they had to consider the expectations and requirements of the developer community as well as optimization issues in language engines.

  • 2016-07 – Stage 2. After the decorators proposal reached stage 2, its API began to undergo significant changes. Furthermore, at one point the proposal was referred to as "ESnext class features for JavaScript." During its development, there were numerous ideas about how decorators could be structured. To get a comprehensive view of the entire history of changes, I recommend reviewing the commits in the proposal's repository. Here is an example of what the decorators API used to look like:

    type Decorator = (args: {
      kind: 'method' | 'property' | 'field'
      key: string | symbol
      isStatic: boolean
      descriptor: PropertyDescriptor
    }) => {
      kind: 'method' | 'property' | 'field'
      key: string | symbol
      isStatic: boolean
      descriptor: PropertyDescriptor
      extras: unknown[]
    }

    By the end of stage 2, the decorator API looked as follows:

    type Decorator = (
      value: DecoratedValue,
      context: {
        kind: 'class' | 'method' | 'getter' | 'setter' | 'field' | 'accessor'
        name: string | symbol
        access?: {
          get?: () => unknown
          set?: (value: unknown) => void
        }
        private?: boolean
        static?: boolean
        addInitializer?: (initializer: () => void) => void
      },
    ) => UpdatedDecoratedValue | void
    
    function debounce(delay: number): UpdatedDecoratedValue {
      return (value, context) => {
        let lastTimeout = null
    
        return (...args) => {
          clearInterval(lastTimeout)
    
          lastTimeout = setTimeout(() => value.call(null, ...args), delay)
        }
      }
    }

    The entire stage 2 took 6 years, during which the decorator API underwent significant changes. However, as we can see from the code above, mutations were excluded. This made the proposal more acceptable for JS engines as well as for various platforms, frameworks, and libraries. But the development history of decorators is not over yet.

  • 2020-09 – Announcing MobX 6. Bye-bye Decorators. Some libraries that relied exclusively on decorators started to move away from their old implementation because they understood that the way they were working with decorators would no longer be standardized.

    Using decorators is no longer the norm in MobX. This is good news to some of you, but others will hate it. Rightfully so, because I concur that the declarative syntax of decorators is still the best that can be offered. When MobX started, it was a TypeScript only project, so decorators were available. Still experimental, but obviously they were going to be standardized soon. That was my expectation at least (I did mostly Java and C# before). However, that moment still hasn't come yet, and two decorators proposals have been cancelled in the mean time. Although they still can be transpiled.” © Michel Weststrate, author of MobX

  • 2022-03 – Stage 3. After years of changes and refinements, decorators finally reached stage 3. Thanks to the extensive adjustments and refinements during the second stage, the third stage began without significant changes. A particular highlight is the creation of a new proposal called Decorator Metadata.

  • 2022-08 – SpiderMonkey Newsletter. SpiderMonkey, the browser engine used by Firefox, became the first engine to begin working on the implementation of decorators. Implementations like this indicate that the proposal is generally ready to become a full-fledged part of the specification.

  • 2022-09 – Babel 7.19.0. Stage 3 decorators. Adding support in a compiler is a very significant update for any proposal. Most proposals have a similar item in their standardization plan and the decorators proposal was no exception.

  • 2022-11 – Announcing TypeScript 4.9. ECMAScript decorators were listed in TS 4.9 Iteration Plan. However, after some time, the TS team decided to move decorators to the 5.0 release. Here is the authors' comment:

    While decorators have reached stage 3, we saw some behavior in the spec that needed to be discussed with the champions. Between addressing that and reviewing the changes, we expect decorators will be implemented in the next version.”

    In general, this decision makes sense, as they did not want to risk incorporating a feature into TS prematurely, especially if it did not become part of the standard. There is always a chance of such situations happening. Although in this case, it might not be as significant as the first implementation.

    In TS 4.9, only a small part of decorators specification was included – Class Auto-Accessors. This addition to the decorators specification served as a correction for the mutations that were prevalent in the first stages of implementation. The reason behind this is that often there is a desire to make properties reactive, meaning that some effects should occur when the property changes, such as UI re-rendering, for example:

    class Dashboard extends HTMLElement {
      @reactive
      tab = DashboardTab.USERS
    }

    In the old implementation, with the reactive decorator, you had to mutate the target class by adding additional set and get accessors to achieve the desired behavior. With the use of auto-accessors, this behavior now occurs more explicitly, which in turn allows engines to optimize it better.

    class Dashboard extends HTMLElement {
      @reactive
      accessor tab = DashboardTab.USERS
    }

    Another interesting thing is how decorators were supposed to work. Since the TS team could not remove the old implementation that worked under the --experimentalDecorators flag, they decided on the following approach: if the --experimentalDecorators flag is present in the configuration, the old implementation will be used. If this flag is not present, then the new implementation will be used.

  • 2023-03 – Announcing TypeScript 5.0. As promised, the TS team released the full version of decorators specification in TS 5.0.

  • 2023-03 – Class Method Parameter Decorators for stage 1. Even before the main decorators proposal is fully implemented, work has begun on significant additions to it, with method parameter decorators undoubtedly being one of these proposals. As we will see, for many library and framework authors, this particular proposal addresses a key functionality gap in the current ECMAScript decorators. This gap has been a major obstacle preventing their migration to the ECMAScript decorators standard.

  • 2023-03 – Deno 1.32. Although in version 1.32 Deno supported TS 5.0, they decided to postpone the functionality related to decorators.

    Take note that ES decorators are not yet supported, but we will be working towards enabling them by default in a future version.”

  • 2023-05 – Angular v16 is here. Angular 16 also added support for ECMAScript decorators. However, some other frameworks built around decorators (and which were inspired by Angular?) have stated that they will not make changes toward ECMAScript decorators for now. For many of them, two important aspects are Metadata and Parameter decorators.

    I don't think we'll support JS decorators till the metadata support & parameter decorators are implemented.” © Kamil Mysliwiec, creator of NextJS

  • 2023-08 – Announcing TypeScript 5.2. In TS 5.2, another standard was added that complements the decorators specification – Decorator Metadata. The primary idea behind this proposal is to simplify decorators' access to class metadata in which they are used. Another reason there were so many debates regarding syntax and usage was that the authors had to create a whole separate proposal for this purpose.

  • 2023-09 – Babel 7.23.0. Decorator Metadata. Over time, Babel introduced the decorator metadata proposal. What is noteworthy is that while they typically have one plugin per proposal, the decorators proposal and the decorator metadata proposal are so closely related that they opted against creating a separate plugin for the decorator metadata proposal. Instead, they incorporated it directly into the main decorators proposal plugin. This underscores the significance of decorator metadata within the overall decorators proposal.

  • 2023-10 – Lit 3.0. Hello TC39 Decorators. Lit has always been a decorators-first framework, though it could have been used without decorators. The arrival of standard decorators allows them to begin the process of moving to a decorator implementation that will not require a compiler to use. For Lit, as a framework based on the most modern JavaScript and browser APIs, this is undoubtedly an important step.

  • 2023-11 – MobX + Standard Decorators. MobX 6.11 added support for native decorators. Interestingly, according to their benchmarks, standard decorators incur 30% less runtime overhead, so they strongly recommend upgrading. Additionally, MobX has supported the old decorator implementation all this time. With this update, and the introduction of support for native decorators, they plan to completely remove the old implementation in version 7.

  • 2024-01 – Deno 1.40. Deno 1.40 became the first platform to include native decorators support. However, it did so with an interesting caveat:

    This feature is available in .ts, .jsx, and .tsx files. Support in pure JavaScript is waiting on implementation in V8.

    No one dares to add full support yet, as there is a fear of repeating the issues encountered with the first implementation of decorators.

  • 2024-02 – Function Decorators for stage 1. This proposal is the first to introduce functionality that has not been present in any previous decorator implementation and does not pertain to classes, although the interface for working with all decorators is similar. Practically all further extensions of the decorator proposal add new values to context.kind. Let's hope that the extensive work on the main interface was not in vain, and that future proposals related to decorators, including this proposal for function decorators, will take less time to implement.

  • 2024-05 – esbuild 0.21.0. The author of esbuild added support for the decorators proposal to the package. Notably, during the implementation, the author discovered a bunch of issues in other packages, such as Babel, TypeScript, and others. This highlights the fact that the decorators proposal is very complex, something the author of esbuild has repeatedly noted. A few days after the release of version 0.21.0, the author also released version 0.21.3, which added support for the decorator metadata proposal.

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:lodash@4.17.21'

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.

Webmentions

If you liked this article and think others should read it, please share it. Leave comments on platforms such as dev.to, twitter.com, etc., and they will magically appear here ✨

  • 0 reposts
  • 5 comments
  • @lexiebkm portrait.

    @lexiebkm

    I thought decorators were only available in TS not JS. I have seen Angular rely on them to define modules and components; that's why it requires TS.
    NestJs too, prefers TS so that it can use decorators to define controller, routing/endpoints for HTTP verbs (get, post, etc).
    And we know Java has annotations that serve like TS decorators; we see Spring use annotations for defining REST controller, routing/endpoints for HTTP verbs, similar to what NestJs does with decorators. Oh, maybe NestJs was inspired by Spring in this aspect.

    I still lack in TS, that's why I cannot continue learning Angular and NestJs, although the latter supports JS too (but not full support, I think).
    Now, with JS through newest ES provides decorators, I can expect to learn NestJs using JS.
    Oh.. I am expecting Angular will support JS too. … see more

  • @what1s1ove portrait.

    @what1s1ove

    Hey @lexiebkm !

    I thought decorators were only available in TS not JS. I have seen Angular rely on them to define modules and components; that's why it requires TS … see more

  • @lizaveis portrait.

    @lizaveis

    Great article! Thanks for sharing your insights

  • @lexiebkm portrait.

    @lexiebkm

    Thanks for your reply and explanation.
    I think, I can wait when decorators become standard. Your examples of using them here are impressive, especially with HOC.

  • @what1s1ove portrait.

    @what1s1ove

    Thank you @lexiebkm ! I really love “live” examples. Everything is easier with them!

These are webmentions via the IndieWeb and webmention.io. Mention this post from your site:

Not an Easter Egg (but actually it is, yes 😁)