Design PatternsJavaScriptMedium

Explain the decorator pattern

01

The Short Answer

The decorator pattern is a structural design pattern that lets you attach new behavior to an object dynamically — without modifying its original code or using inheritance. You wrap the original object in a 'decorator' that has the same interface but adds extra functionality before or after delegating to the wrapped object. In JavaScript, this shows up as higher-order functions, class decorators (TC39 proposal), and wrapper patterns in React (HOCs).

02

The Core Idea

Think of it like adding toppings to a pizza. The base pizza is your original object. Each topping (decorator) wraps the pizza and adds something new — cheese, pepperoni, mushrooms — without changing the base pizza itself. You can stack as many decorators as you want, and each one only knows about the thing it wraps, not the full chain. The key constraint is that the decorator has the same interface as the thing it wraps, so the rest of your code doesn't know (or care) whether it's talking to the original or a decorated version.

🎁

Gift Wrapping Analogy

A decorated object is like a gift inside multiple layers of wrapping paper. Each layer adds something (a bow, a tag, glitter) but the gift inside stays unchanged. You can unwrap layers independently, and the outermost layer is what the world sees — same shape as the gift, just enhanced.

03

Function Decorators (Higher-Order Functions)

The simplest form of the decorator pattern in JavaScript is a higher-order function that takes a function, wraps it with extra behavior, and returns a new function with the same signature. This is the most common and idiomatic way to use decorators in JS — no classes needed. You've probably used this pattern without realizing it (debounce, throttle, memoize are all decorators).

function-decorators.tstypescript
// A decorator is a function that wraps another function
// and adds behavior without modifying the original

// Logging decorator — adds console output around any function
function withLogging<T extends (...args: any[]) => any>(fn: T, label: string): T {
  return ((...args: Parameters<T>) => {
    console.log(`[${label}] called with:`, args);
    const result = fn(...args);
    console.log(`[${label}] returned:`, result);
    return result;
  }) as T;
}

// Timing decorator — measures execution time
function withTiming<T extends (...args: any[]) => any>(fn: T, label: string): T {
  return ((...args: Parameters<T>) => {
    const start = performance.now();
    const result = fn(...args);
    console.log(`[${label}] took ${(performance.now() - start).toFixed(2)}ms`);
    return result;
  }) as T;
}

// Memoization decorator — caches results for repeated calls
function withMemoization<T extends (...args: any[]) => any>(fn: T): T {
  const cache = new Map<string, ReturnType<T>>();
  return ((...args: Parameters<T>) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key)!;
    const result = fn(...args);
    cache.set(key, result);
    return result;
  }) as T;
}

// Usage — stack decorators by wrapping repeatedly
function fetchUserData(userId: string) {
  // expensive API call
  return fetch(`/api/users/${userId}`).then(r => r.json());
}

// Decorate: add logging + memoization without touching the original
const enhancedFetch = withLogging(withMemoization(fetchUserData), 'fetchUser');
// enhancedFetch has the same signature as fetchUserData
// but now it logs calls AND caches results

Notice how each decorator is independent — withLogging doesn't know about withMemoization and vice versa. You can compose them in any order. The original fetchUserData function is never modified. This is the Open/Closed Principle in action: open for extension (add logging, caching) but closed for modification (original function untouched).

04

Class-Based Decorators

In object-oriented code, the decorator pattern uses classes that implement the same interface as the object they wrap. The decorator holds a reference to the wrapped object and delegates most calls to it, adding behavior only where needed. This is the 'classic' GoF (Gang of Four) version of the pattern, and it's useful when you need to decorate objects with complex interfaces.

class-decorators.tstypescript
// Interface that both the original and decorators implement
interface DataSource {
  read(): string;
  write(data: string): void;
}

// Base implementation
class FileDataSource implements DataSource {
  constructor(private filename: string) {}

  read(): string {
    return `contents of ${this.filename}`;
  }

  write(data: string): void {
    console.log(`Writing to ${this.filename}: ${data}`);
  }
}

// Decorator base — delegates everything to the wrapped object
class DataSourceDecorator implements DataSource {
  constructor(protected wrapped: DataSource) {}

  read(): string {
    return this.wrapped.read();
  }

  write(data: string): void {
    this.wrapped.write(data);
  }
}

// Encryption decorator — encrypts on write, decrypts on read
class EncryptionDecorator extends DataSourceDecorator {
  read(): string {
    const data = this.wrapped.read();
    return this.decrypt(data);
  }

  write(data: string): void {
    this.wrapped.write(this.encrypt(data));
  }

  private encrypt(data: string): string {
    return btoa(data); // simplified
  }

  private decrypt(data: string): string {
    return atob(data); // simplified
  }
}

// Compression decorator — compresses on write, decompresses on read
class CompressionDecorator extends DataSourceDecorator {
  write(data: string): void {
    const compressed = this.compress(data);
    this.wrapped.write(compressed);
  }

  private compress(data: string): string {
    return `[compressed: ${data.length} chars]`;
  }
}

// Stack decorators — order matters!
const source = new FileDataSource('config.json');
const encrypted = new EncryptionDecorator(source);
const compressedAndEncrypted = new CompressionDecorator(encrypted);

// Writing goes: compress → encrypt → write to file
// Reading goes: read from file → decrypt → decompress
compressedAndEncrypted.write('sensitive data');

The class-based approach is more verbose than function decorators, but it shines when the interface has multiple methods. Each decorator only overrides the methods it cares about and delegates the rest — you don't need to re-implement the entire interface in every decorator.

05

TC39 Decorators (Stage 3)

JavaScript has a TC39 proposal (Stage 3) for native decorator syntax using the @ symbol. TypeScript has supported an older version of this for years (with experimentalDecorators). These decorators are syntactic sugar for wrapping class methods, properties, or entire classes — they're the same pattern, just with cleaner syntax. The new standard decorators work differently from TypeScript's legacy ones, so be aware of which version you're using.

tc39-decorators.tstypescript
// TC39 Stage 3 decorator syntax (2024+)
// Decorators are functions that receive the method/class and return a replacement

// Method decorator — wraps a class method
function logged(
  target: any,
  context: ClassMethodDecoratorContext
) {
  const methodName = String(context.name);
  return function (this: any, ...args: any[]) {
    console.log(`→ ${methodName}(${args.join(', ')})`);
    const result = target.call(this, ...args);
    console.log(`← ${methodName} returned:`, result);
    return result;
  };
}

// Bound decorator — auto-binds method to instance
function bound(
  target: any,
  context: ClassMethodDecoratorContext
) {
  context.addInitializer(function (this: any) {
    this[context.name] = target.bind(this);
  });
  return target;
}

// Usage with @ syntax
class UserService {
  private users: string[] = [];

  @logged
  addUser(name: string) {
    this.users.push(name);
    return this.users.length;
  }

  @bound
  @logged
  removeUser(name: string) {
    this.users = this.users.filter(u => u !== name);
    return true;
  }
}

const service = new UserService();
service.addUser('Alice');
// Console: → addUser(Alice)
// Console: ← addUser returned: 1

// @bound means you can pass the method as a callback safely
const remove = service.removeUser;
remove('Alice'); // 'this' is still bound to service
06

Decorators in React (HOCs)

Higher-Order Components (HOCs) in React are the decorator pattern applied to components. A HOC takes a component and returns a new component that wraps the original with extra behavior — authentication checks, data fetching, logging, theming, etc. While hooks have largely replaced HOCs for new code, understanding them as decorators helps you recognize the pattern across different contexts.

react-hoc-decorator.tsxtsx
import { ComponentType, useEffect, useState } from 'react';

// HOC = decorator pattern for React components
// Takes a component, returns an enhanced version

function withAuth<P extends object>(WrappedComponent: ComponentType<P>) {
  return function AuthenticatedComponent(props: P) {
    const [isAuthenticated, setIsAuthenticated] = useState(false);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
      checkAuth().then(authed => {
        setIsAuthenticated(authed);
        setLoading(false);
      });
    }, []);

    if (loading) return <div>Checking auth...</div>;
    if (!isAuthenticated) return <div>Please log in</div>;

    // Delegates to the wrapped component — same interface
    return <WrappedComponent {...props} />;
  };
}

function withErrorBoundary<P extends object>(WrappedComponent: ComponentType<P>) {
  return function WithErrorBoundary(props: P) {
    // Simplified — real implementation uses class component
    return (
      <ErrorBoundary fallback={<div>Something went wrong</div>}>
        <WrappedComponent {...props} />
      </ErrorBoundary>
    );
  };
}

// Stack decorators — same composition as function decorators
function Dashboard() {
  return <div>Secret Dashboard</div>;
}

// Decorated: auth check + error boundary + original component
export default withErrorBoundary(withAuth(Dashboard));
07

Decorator vs Inheritance

AspectDecorator PatternInheritance
CompositionRuntime — wrap objects dynamicallyCompile-time — fixed class hierarchy
FlexibilityMix and match behaviors freelySingle inheritance chain (JS)
CouplingLow — decorators are independentHigh — subclass depends on parent
Adding behaviorStack multiple decoratorsEach new behavior = new subclass
Removing behaviorUnwrap the decoratorCan't remove parent behavior easily
InterfaceMust match wrapped object's interfaceInherits entire parent interface
Combinatorial explosionN decorators = N classesN behaviors = 2^N subclass combinations
08

Why Interviewers Ask This

The decorator pattern tests your understanding of composition over inheritance — a fundamental principle in modern software design. Interviewers want to see that you can identify when wrapping is better than extending, know practical examples (middleware, HOCs, debounce/throttle), understand the Open/Closed Principle (extend behavior without modifying existing code), can implement decorators in both functional and class-based styles, and recognize the pattern in frameworks you use daily (Express middleware, React HOCs, TypeScript decorators).

Quick Revision Cheat Sheet

What it does: Wraps an object to add behavior without modifying the original

JS form: Higher-order functions — debounce, throttle, memoize are all decorators

Class form: Wrapper class with same interface, delegates to wrapped object

React form: Higher-Order Components (HOCs) — withAuth, withTheme, connect()

Key principle: Open/Closed — open for extension, closed for modification

vs Inheritance: Decorators compose at runtime; inheritance is fixed at compile time