JavaScript Decorators and Metadata Explained

JavaScript decorators and metadata represent a major evolution in the ECMAScript standard, providing developers with declarative tools to inspect, modify, and enhance classes and their members. This article explains the fundamentals of modern ECMAScript decorators, details how the associated Metadata API works, explores common use cases like dependency injection and validation, and clarifies how these features streamline metaprogramming in modern JavaScript.

What Are Decorators?

Decorators are functions applied to classes, methods, fields, getters, setters, or accessors using the @ prefix syntax. Instead of altering behavior at runtime via inheritance or manual wrapping, decorators execute once when the class is defined, allowing you to intercept, wrap, or modify the target construct.

In the standard ECMAScript specification (Stage 3), a decorator function receives two primary arguments:

  1. Value: The element being decorated (e.g., a method function, class definition, or undefined for fields).
  2. Context: An object containing metadata and helper utilities for the element being decorated.

The context object provides structured information, including: - kind: The type of member being decorated ('class', 'method', 'getter', 'setter', 'field', or 'accessor'). - name: The name of the class or member (as a string or symbol). - static: A boolean indicating if the member is a static class member. - private: A boolean indicating if the member is a private class member (#member). - access: An object containing get and set functions to access the member. - addInitializer: A function enabling developers to schedule logic to run during instance initialization.

How Decorators Work in Practice

Here is a basic example of a logging decorator in modern JavaScript:

function logged(value, context) {
  const { kind, name } = context;

  if (kind === "method") {
    return function (...args) {
      console.log(`Calling ${name} with arguments:`, args);
      const result = value.apply(this, args);
      console.log(`Result from ${name}:`, result);
      return result;
    };
  }
}

class Calculator {
  @logged
  add(a, b) {
    return a + b;
  }
}

const calc = new Calculator();
calc.add(2, 3);

What is Decorator Metadata?

Historically, attaching metadata to classes required external libraries such as reflect-metadata. The standard ECMAScript Decorator Metadata proposal formalizes this capability directly into JavaScript.

The context object includes a metadata property, which is a plain JavaScript object shared across all decorators applied to a given class and its members. Any properties attached to context.metadata are aggregated and stored under the well-known symbol Symbol.metadata on the class constructor.

Example of Attaching and Reading Metadata

function tag(tagName) {
  return function (value, context) {
    context.metadata.tags = context.metadata.tags || [];
    context.metadata.tags.push(tagName);
  };
}

@tag("UserManagement")
class UserService {
  @tag("Sensitive")
  deleteUser(id) {
    // deletion logic
  }
}

// Accessing metadata via Symbol.metadata
const metadata = UserService[Symbol.metadata];
console.log(metadata.tags); // Output: ['Sensitive', 'UserManagement']

Key Use Cases for Decorators and Metadata

Current Adoption

Modern decorators and metadata represent a standardized consensus that replaces legacy, non-standard implementations previously used in early versions of TypeScript and Babel. As the proposal reaches full finalization in the ECMAScript standard, native runtime support and modern toolchain compatibility continue to expand across Node.js, Deno, Bun, and browser engines.