Decorator Pattern (Design)
In this page:
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
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
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
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
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
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: