visit
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.
“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.”
©
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
.
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.
const debounce = (fn, delay) => {
let lastTimeout = null
return (...args) => {
clearInterval(lastTimeout)
lastTimeout = setTimeout(() => fn.call(null, ...args), delay)
}
}
class SearchForm {
constructor() {
this.handleUserInput = debounce(this.handleUserInput, 300)
}
handleUserInput(evt) {
console.log(evt.target.value)
}
}
class SearchForm {
@debounce(300)
handleUserInput(evt) {
console.log(evt.target.value)
}
}
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}
/>
)
}
}
const AuthPopup = ({ onModalVisibilityToggle }) => {
// Component
}
const WrappedAuthPopup = withModal(AuthPopup)
export { WrappedAuthPopup as AuthPopup }
@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.
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?
@withForm({ mode: 'submit' })
@withFocusTrap()
@withModal()
const AuthPopup = ({
onSubmit,
onFocusTrapInit,
onModalVisibilityToggle,
}) => {
// Component
}
export { AuthPopup }
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.
. 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
}
}
. Without significant changes, the proposal advanced to stage 2. However, an event occurred that significantly influenced the further development of this proposal: , 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.
. 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 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[]
}
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)
}
}
}
. 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
. 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 .
. 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.
. 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 .
. ECMAScript decorators were listed in . However, after some time, the TS team decided to move decorators to the 5.0 release. Here is the :
“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.”
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.
. As promised, the TS team released the full version of decorators specification in TS 5.0.
. 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.”
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, 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
. In TS 5.2, another standard was added that complements the decorators specification – . 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.
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.
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.
The "" 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, 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 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.