← Back to list

Structural Design Pattern: Decorator Pattern — Typescript

Overview

sparshneel chanchlani in LearnABTech · 2026-05-19 06:21 · 0 claps · 2.5 min read
#software-design-patterns #decorator-design-pattern #structural-design-pattern #coffee-decorator-pattern
Open on Medium ↗
Wiki topics: 🌐 · Web Development 🍳 · Food & Cooking

Structural Design Pattern: Decorator Pattern — Typescript

Overview

The Decorator design pattern allows you to dynamically attach new behaviors and responsibilities to objects by placing them inside special wrapper objects. Instead of creating a massive tree of subclasses to handle every possible feature combination, you wrap a base object with the exact features you need. Because behaviors are added using wrappers, you can change or stack an object’s capabilities at runtime based on user input, rather than fixing them at compile time.

The Coffee example

Imagine a coffee shop that offers a variety of drinks on its menu. Every customer has their own preference, and each type of coffee requires a different brewing process. As a software engineer, if you were designing a system to handle all these different orders, you would look for a design pattern that fits this perfectly. This is where the Decorator pattern steps in — allowing us to dynamically ‘brew’ complex objects by wrapping a simple base in layers of custom functionality.

Consider the diagram below to understand better how the decorator pattern helps design the brewing process.

Let’s Code the Coffee Brewing example

The implementation follows the above-mentioned diagram explaining the design of brew process using the Decorator Pattern

Beverage Interface

interface BeverageInterface {
    brew(): Beverage;
    calculatePrice(price: number): void;
    displayPrice(): void;
    addAddOn(addOn: BeverageAddOn): void;
}

type Beverage = {
    name: string;
    price: number;
    status: string;
}

The concrete classes implementing the Beverage Interface. Below is the coffee class implementation considering the design in the above-mentioned diagram. Here, the design keeps one concrete implementation of coffee, and adds decorators to the base class to brew the coffee variety like Cafe Mocha, etc.

Coffee Class

class Coffee implements BeverageInterface {
    private name: string;
    private price: number;
    private static basePrice: number = 5;

    constructor(name: string) {
        this.name = name;
        this.price = Coffee.basePrice;
    }

     brew(): Beverage {
        console.log("brewing: " + this.name);
        switch (this.name) {
            case "Mocha":
                this.addAddOn(new SteamedMilkAddOn(this));
                this.addAddOn(new ChocolateAddOn(this));
                break;
            case "OatMilkMocha":
                this.addAddOn(new OatMilkAddOn(this));
                this.addAddOn(new ChocolateAddOn(this));
                break;
            case "Latte":
                this.addAddOn(new SteamedMilkAddOn(this));
                break;
            default:
                this.addAddOn(new SteamedMilkAddOn(this));
        }
        return {
            name: this.name,
            price: this.price,
            status: "Ready"
        }
    }

    calculatePrice(addOnPrice: number): void {
        this.price += addOnPrice;
    }

    displayPrice(): void {
        console.log("displayPrice: " + this.price);
    }
}

Referring to the above-mentioned design diagram, let’s add the decorators that would enable brewing different coffee varieties.

Interface BeverageAddOn

interface BeverageAddOn {
    calculatePrice(): void;
}

As mentioned in the above diagram, the coffee shop, which currently has three varieties of coffee available on the menu

  • Cafe Latte
  • Cafe Mocha
  • Cafe Mocha with Oat Milk

Based on the menu availability, below are the concrete implementations of BeverageAddOn

Steamed Milk

class SteamedMilkAddOn implements BeverageAddOn {
    private coffee: BeverageInterface;
    private static addOnPrice: number = 2;
    constructor(coffee: BeverageInterface) {
        this.coffee = coffee;
    }
    calculatePrice(): void {
        this.coffee.calculatePrice(SteamedMilkAddOn.addOnPrice)
    }
}

Chocolate(choco syrup or powder)

class ChocolateAddOn implements BeverageAddOn {

    private coffee: BeverageInterface;

    private static addOnPrice: number = 3;

    constructor(coffee: BeverageInterface) {
        this.coffee = coffee;
    }
    calculatePrice(): void {
        this.coffee.calculatePrice(ChocolateAddOn.addOnPrice)
    }
}

Oat Milk

class OatMilkAddOn implements BeverageAddOn {

    private coffee: BeverageInterface;
    private static addOnPrice: number = 4;

    constructor(coffee: BeverageInterface) {
        this.coffee = coffee;
    }
    calculatePrice(): void {
        this.coffee.calculatePrice(OatMilkAddOn.addOnPrice)
    }
}

Putting It All Together

Main

import {BeverageInterface} from "./interfaces/Beverage.ts";
import {Coffee} from "./concerete-classes/Coffee.ts";

const mocha: BeverageInterface = new Coffee("OatMilkMocha");
const brew = mocha.brew(); 
console.log("Brewed Coffee:", brew);

Test Results

brewing: OatMilkMocha
Brewed Coffee: { name: 'OatMilkMocha', price: 12, status: 'Ready' }

Conclusion

The Decorator pattern is your best friend when you need to add responsibilities to objects without messing with the original code. It keeps your classes small, your logic separated, and your coffee orders infinitely customizable.


메타데이터
post_id
c9f8fa54e2f5
slug
structural-design-pattern-decorator-pattern-typescript-c9f8fa54e2f5
url
https://medium.com/the-quick-learners/structural-design-pattern-decorator-pattern-typescript-c9f8fa54e2f5
canonical_url
https://medium.com/the-quick-learners/structural-design-pattern-decorator-pattern-typescript-c9f8fa54e2f5
author_url
https://medium.com/@sparshneel.chanchlani
status
ok
fetched_at
2026-06-24 04:09:36