JavaScript Class Decorators Explained

JavaScript class decorators are a powerful language feature that allows developers to customize, extend, and annotate classes and their members using a declarative syntax. This article explores what decorators are, why they are useful, and how the official TC39 Stage 3 standard proposal defines their structure, context object, and execution lifecycle in modern JavaScript.

What Are JavaScript Class Decorators?

A decorator is essentially a higher-order function applied to a class or a class member (such as a method, field, getter, or setter) using the @ prefix. Decorators allow you to wrap, modify, or observe the behavior of the target element at definition time, promoting code reusability and separating cross-cutting concerns like logging, validation, memoization, and access control.

@logged
class Person {
  @memoize
  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

The TC39 Standard Proposal

Decorators have evolved significantly over the years. Early implementations, commonly known as “legacy” or “experimental” decorators in TypeScript and Babel (Stage 1/2), differed in their mechanics. The current standard is the TC39 Stage 3 Decorators proposal. This standard introduces a cleaner, safer, and more unified API designed to prevent unintended prototype mutations and provide explicit lifecycle hooks.

How the Standard Defines Decorators

Under the standard proposal, a decorator is a function invoked with two arguments:

  1. value: The target being decorated (e.g., the class constructor, method function, or undefined in the case of fields).
  2. context: An object containing metadata and helper utilities related to the target element.

The context Object

The context argument provides strict, read-only metadata about the element being decorated:

Types of Decorators Defined by the Proposal

1. Class Decorators

A class decorator receives the class constructor as value and { kind: "class", name, addInitializer } as context. It can return a new constructor to replace the original class, or return nothing to leave the class unchanged.

function sealed(targetClass, context) {
  if (context.kind === "class") {
    Object.seal(targetClass);
    Object.seal(targetClass.prototype);
  }
}

@sealed
class User {}

2. Method Decorators

A method decorator receives the method function as value. It can return a new function that wraps or replaces the original method.

function logged(originalMethod, context) {
  if (context.kind === "method") {
    return function (...args) {
      console.log(`Calling ${String(context.name)} with args:`, args);
      return originalMethod.apply(this, args);
    };
  }
}

3. Field Decorators

Field decorators receive undefined as value because fields do not have an initial value at the time the class definition is evaluated. A field decorator can return an initializer function that modifies the initial value assigned to the field when an instance is created.

function defaultValue(fallback) {
  return function (value, context) {
    if (context.kind === "field") {
      return (initialValue) => initialValue ?? fallback;
    }
  };
}

4. Auto-Accessor Decorators

The standard proposal introduces the accessor keyword for class fields. Auto-accessors generate a private storage slot along with automatic getter and setter functions. An accessor decorator receives an object containing { get, set } and can return an object replacing the getter, setter, or the initial value.

class State {
  @tracked accessor count = 0;
}

Execution Order

The TC39 proposal defines a strict execution order: 1. Decorator expressions are evaluated from top to bottom, outer to inner. 2. Decorators are applied to class elements from bottom to top, inner to outer. 3. Member decorators run first as the class body is parsed. 4. Class-level decorators run last once the entire class body has been defined.