Decorator Pattern in JavaScript Classes Explained

The Decorator Pattern is a structural design pattern that allows you to dynamically extend or modify the behavior of objects and classes without altering their original source code. In JavaScript, developers can apply this pattern using either traditional higher-order functions or modern ECMAScript class decorators (@decorator). This article explains the fundamental concepts behind the Decorator Pattern, its key benefits, and practical implementations for JavaScript classes and class methods.

What is the Decorator Pattern?

The Decorator Pattern is built on the principle of composition over inheritance. Instead of creating numerous subclasses to add new features to an existing class, you wrap the original class or method inside a decorator function.

This approach adheres to the Open/Closed Principle of software design, which states that software entities should be open for extension but closed for modification. By decorating code, you can inject functionality such as logging, authentication, caching, or validation at runtime without risking bugs in the core business logic.

Traditional Function-Based Decorator in JavaScript

Before formal decorator syntax was introduced to JavaScript, decorators were implemented as higher-order functions that take a target object or function, wrap it with extra behavior, and return it.

class Coffee {
  cost() {
    return 5;
  }
}

// Decorator function
function withMilk(coffee) {
  const originalCost = coffee.cost();
  coffee.cost = function () {
    return originalCost + 2;
  };
  return coffee;
}

const simpleCoffee = new Coffee();
const milkCoffee = withMilk(simpleCoffee);

console.log(milkCoffee.cost()); // Output: 7

Modern JavaScript Class Decorators

Modern JavaScript (via current TC39 proposals and TypeScript) supports native decorator syntax using the @ prefix. Decorators are essentially functions invoked by the runtime with details about the class element being decorated.

1. Class Method Decorator

A method decorator wraps or alters a specific method inside a class. It receives the original method and context information, returning a new function to replace the original.

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

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

const calc = new Calculator();
calc.add(3, 4);
// Logs:
// Calling method "add" with arguments: [3, 4]
// Method "add" returned: 7

2. Class Decorator

A class decorator operates on the entire class constructor, allowing you to add static properties, modify prototype methods, or intercept instantiation.

function freezeClass(target, { kind }) {
  if (kind === "class") {
    Object.freeze(target);
    Object.freeze(target.prototype);
    return target;
  }
}

@freezeClass
class User {
  constructor(name) {
    this.name = name;
  }
}

Benefits of Using Decorators