← Back to TypeScript Course | Chapter 16: Advanced Patterns | Lesson 6 of 7

Decorator Pattern (Design)

The Decorator Pattern adds behavior to an object by wrapping it instead of modifying its original class. Multiple decorators can be combined to build behavior dynamically.

Basic Decorator

A decorator implements the same interface as the component it wraps, delegates the original operation to that component, and adds extra behavior before or after — the caller can't tell it's talking to a decorator instead of the real thing.

Example: Basic Decorator

typescript
interface Coffee {
  cost(): number;
}
class SimpleCoffee implements Coffee {
  cost() { return 5; }
}
class MilkDecorator implements Coffee {
  constructor(private coffee: Coffee) {}
  cost() { return this.coffee.cost() + 1; }
}
console.log(new MilkDecorator(new SimpleCoffee()).cost());

Stacking Decorators

Decorators can wrap other decorators, so several independent behaviors — logging, caching, retries — can be layered onto one object just by nesting the wrapping calls, each layer only knowing about the one it directly wraps.

Example: Stacking Decorators

typescript
interface Coffee {
  cost(): number;
}
class SimpleCoffee implements Coffee {
  cost() { return 5; }
}
class MilkDecorator implements Coffee {
  constructor(private coffee: Coffee) {}
  cost() { return this.coffee.cost() + 1; }
}
class SugarDecorator implements Coffee {
  constructor(private coffee: Coffee) {}
  cost() { return this.coffee.cost() + 0.5; }
}
const order = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));
console.log(order.cost());

Shared Interface

The original component and every decorator must follow the same interface so callers can treat a decorated object exactly like the original, without any special-casing at the call site.

Example: Shared Interface

typescript
interface Notifier {
  send(message: string): void;
}
class EmailNotifier implements Notifier {
  send(message: string) { console.log("Email:", message); }
}
class SmsDecorator implements Notifier {
  constructor(private wrapped: Notifier) {}
  send(message: string) {
    this.wrapped.send(message);
    console.log("SMS:", message);
  }
}
new SmsDecorator(new EmailNotifier()).send("Hello");

Decorator vs Inheritance

Decorator uses composition to add optional behavior at runtime, while inheritance bakes a fixed class hierarchy in at compile time — composition lets you mix and match features far more flexibly than a rigid subclass tree would.

Example: Decorator vs Inheritance

typescript
interface Coffee { cost(): number; }
class SimpleCoffee implements Coffee { cost() { return 5; } }
// Inheritance would need a fixed subclass per combination (MilkCoffee, SugarCoffee, MilkSugarCoffee...)
// Decorator composes them at runtime instead:
class MilkDecorator implements Coffee {
  constructor(private coffee: Coffee) {}
  cost() { return this.coffee.cost() + 1; }
}
console.log(new MilkDecorator(new SimpleCoffee()).cost());

When to Use Decorator

Decorator is a natural fit for optional, composable features such as logging, caching, authorization, formatting, and notifications — anything you might want to add or remove without touching the wrapped object's own code.

Example: When to Use Decorator

typescript
interface Handler {
  handle(req: string): string;
}
class BaseHandler implements Handler {
  handle(req: string) { return `Handled: ${req}`; }
}
class LoggingDecorator implements Handler {
  constructor(private wrapped: Handler) {}
  handle(req: string) {
    console.log("Logging request:", req);
    return this.wrapped.handle(req);
  }
}
console.log(new LoggingDecorator(new BaseHandler()).handle("GET /"));
🔒

Chapter Quiz — Complete all 7 topics to unlock

0/7 topics done

Complete these topics first:

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.