Decorator Pattern

Hard

The Decorator Pattern dynamically adds responsibilities to an object without modifying its original code. In JavaScript, higher-order functions naturally implement this pattern — a decorator wraps a function to extend its behavior while preserving the original interface.

Interactive Visualization

Creational Patterns

IIFE creates private scope, returns public API via closure
returnscloses overcallsIIFE Scopeprivate varsPublic APIConsumer
1 / 3

Understanding Decorator Pattern

The Decorator Pattern adds behavior to an object or function dynamically, without modifying its source code. In JavaScript, this pattern is implemented most naturally through higher-order functions — functions that take a function and return a new function with enhanced behavior.

Every middleware in Express, every Higher-Order Component in React, and every function wrapper that adds logging, caching, or authentication is a decorator. The pattern is so natural in JavaScript that developers often use it without realizing they are applying a formal design pattern. A decorator wraps the original, adds behavior before or after the call, and delegates to the original to preserve its core functionality.

The power of decorators comes from composition. Multiple decorators can be stacked, and each one is independent and reusable. A pipe() or compose() utility makes the stacking explicit and readable. The TC39 decorators proposal brings first-class @decorator syntax to JavaScript classes, but the higher-order function approach works everywhere and does not require any new syntax.

Key Points

  • Adds behavior to individual objects without modifying the class or prototype
  • Decorators wrap the original and delegate to it, preserving the interface
  • In JavaScript, higher-order functions are the idiomatic decorator mechanism
  • Can be composed — multiple decorators stack on top of each other
  • TC39 decorators proposal (@decorator syntax) brings first-class support to classes
  • Common uses: logging, timing, caching, authentication, retry logic

Code Examples

Function Decorator (HOF)

function withLogging(fn) {
  return function (...args) {
    console.log('Calling', fn.name, 'with', args);
    const result = fn(...args);
    console.log('Result:', result);
    return result;
  };
}

function withTiming(fn) {
  return function (...args) {
    const start = performance.now();
    const result = fn(...args);
    console.log(fn.name, 'took', (performance.now() - start).toFixed(2), 'ms');
    return result;
  };
}

function add(a, b) { return a + b; }

const enhanced = withLogging(withTiming(add));
enhanced(2, 3);
// "Calling add with [2, 3]"
// "add took 0.01 ms"
// "Result: 5"

Each decorator wraps the function, adding behavior before or after. They compose from inside out.

Composable Decorators with pipe

const pipe = (...fns) => (fn) => fns.reduce((acc, decorator) => decorator(acc), fn);

function withRetry(attempts) {
  return (fn) => async function (...args) {
    for (let i = 0; i < attempts; i++) {
      try { return await fn(...args); }
      catch (err) {
        if (i === attempts - 1) throw err;
        console.log('Retry', i + 1);
      }
    }
  };
}

function withCache(fn) {
  const cache = new Map();
  return function (...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

const enhance = pipe(withLogging, withCache, withRetry(3));
const fetchUser = enhance(async (id) => {
  const res = await fetch('/api/users/' + id);
  return res.json();
});

pipe composes decorators left-to-right. Each decorator is independent and reusable.

Object Property Decorator

function readonly(target, key, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

function memoize(target, key, descriptor) {
  const original = descriptor.value;
  const cache = new Map();
  descriptor.value = function (...args) {
    const cacheKey = JSON.stringify(args);
    if (cache.has(cacheKey)) return cache.get(cacheKey);
    const result = original.apply(this, args);
    cache.set(cacheKey, result);
    return result;
  };
  return descriptor;
}

class MathUtils {
  // @readonly — future TC39 syntax
  static PI = 3.14159;

  // @memoize — future TC39 syntax
  fibonacci(n) {
    if (n <= 1) return n;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

Property descriptors enable method-level decoration. The TC39 proposal formalizes the @decorator syntax.

Common Mistakes

  • Breaking the original interface — a decorator must accept and return the same shape
  • Losing function metadata (name, length) when wrapping — use Object.defineProperty to preserve it
  • Applying too many decorators, creating a deeply nested call stack that is hard to debug
  • Not handling async correctly in decorators that wrap Promise-returning functions
  • Confusing the Decorator Pattern with inheritance — decorators compose, inheritance extends

Interview Tips

  • Show that higher-order functions ARE decorators — withAuth(handler), withLogging(fn)
  • Explain composition: decorators can stack, and order matters
  • Mention real examples: Express/Koa middleware, React HOCs, TypeScript experimentalDecorators
  • Discuss the TC39 decorators proposal and its current stage

Related Concepts