StructuralTypeScriptverifiedVerified
Decorator Pattern in TypeScript
Attaches additional responsibilities to an object dynamically by wrapping it in decorator objects that share the same interface.
How to Implement the Decorator Pattern in TypeScript
1Step 1: Define the component interface and base implementation
interface Component {
operation(): string;
}
class ConcreteComponent implements Component {
operation(): string {
return "ConcreteComponent";
}
}2Step 2: Create the base decorator and concrete decorators
class BaseDecorator implements Component {
protected wrappee: Component;
constructor(component: Component) {
this.wrappee = component;
}
operation(): string {
return this.wrappee.operation();
}
}
class ConcreteDecoratorA extends BaseDecorator {
operation(): string {
return `DecoratorA(${super.operation()})`;
}
}
class ConcreteDecoratorB extends BaseDecorator {
operation(): string {
return `DecoratorB(${super.operation()})`;
}
}3Step 3: Compose decorators and observe layered behavior
function clientCode(component: Component): void {
console.log("Result:", component.operation());
}
const simple = new ConcreteComponent();
clientCode(simple);
// Result: ConcreteComponent
const decorated = new ConcreteDecoratorB(new ConcreteDecoratorA(simple));
clientCode(decorated);
// Result: DecoratorB(DecoratorA(ConcreteComponent))// Decorator Pattern – Production
// Transparent logging and in-memory caching decorators
// layered over any async service method.
export interface UserService {
getUser(id: string): Promise<{ id: string; name: string }>;
}
// ── Concrete service ──────────────────────────────────────────────────────────
export class RealUserService implements UserService {
async getUser(id: string) {
// Simulates an I/O call
return { id, name: `User ${id}` };
}
}
// ── Logging decorator ─────────────────────────────────────────────────────────
export class LoggingUserService implements UserService {
constructor(private readonly inner: UserService) {}
async getUser(id: string) {
console.log(`[LOG] getUser called with id=${id}`);
const start = Date.now();
try {
const result = await this.inner.getUser(id);
console.log(`[LOG] getUser succeeded in ${Date.now() - start}ms`);
return result;
} catch (err) {
console.error(`[LOG] getUser failed after ${Date.now() - start}ms`, err);
throw err;
}
}
}
// ── Caching decorator ─────────────────────────────────────────────────────────
export class CachingUserService implements UserService {
private cache = new Map<string, { value: { id: string; name: string }; expiresAt: number }>();
constructor(
private readonly inner: UserService,
private readonly ttlMs: number = 60_000,
) {}
async getUser(id: string) {
const cached = this.cache.get(id);
if (cached && Date.now() < cached.expiresAt) {
console.log(`[CACHE] HIT for id=${id}`);
return cached.value;
}
console.log(`[CACHE] MISS for id=${id}`);
const value = await this.inner.getUser(id);
this.cache.set(id, { value, expiresAt: Date.now() + this.ttlMs });
return value;
}
}
// ── Composition ────────────────────────────────────────────────────────────────
// Logging → Caching → Real service
const service: UserService = new LoggingUserService(
new CachingUserService(new RealUserService(), 30_000),
);
// First call: cache miss, then real fetch, then logged
service.getUser("abc-123").then(console.log);
// Second call: served from cache
service.getUser("abc-123").then(console.log);Decorator Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Decorator Pattern in the Real World
“Think of adding espresso shots and syrups to a coffee order. A plain coffee is the base component. Each addition—an espresso shot, vanilla syrup, oat milk—is a decorator that wraps the previous cup, adding its own cost and flavor. You can combine them in any order without the café needing a separate menu item for every combination.”