+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 291 of 355

๐Ÿ“˜ Decorator Pattern: Dynamic Extension

Master decorator pattern: dynamic extension in TypeScript with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

Prerequisites

  • Basic understanding of JavaScript ๐Ÿ“
  • TypeScript installation โšก
  • VS Code or preferred IDE ๐Ÿ’ป

What you'll learn

  • Understand the concept fundamentals ๐ŸŽฏ
  • Apply the concept in real projects ๐Ÿ—๏ธ
  • Debug common issues ๐Ÿ›
  • Write type-safe code โœจ

๐ŸŽฏ Introduction

Welcome to this exciting tutorial on the Decorator Pattern! ๐ŸŽ‰ In this guide, weโ€™ll explore how to dynamically extend object functionality without modifying the original code.

Youโ€™ll discover how the Decorator Pattern can transform your TypeScript development experience. Whether youโ€™re building web applications ๐ŸŒ, game engines ๐ŸŽฎ, or e-commerce platforms ๐Ÿ›’, understanding this pattern is essential for writing flexible, maintainable code.

By the end of this tutorial, youโ€™ll feel confident using decorators to add superpowers to your objects! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Decorator Pattern

๐Ÿค” What is the Decorator Pattern?

The Decorator Pattern is like adding toppings to a pizza ๐Ÿ•. Think of it as wrapping gifts ๐ŸŽ - each wrapper adds a new layer of functionality while preserving whatโ€™s inside.

In TypeScript terms, decorators allow you to add new behaviors to objects dynamically without altering their structure. This means you can:

  • โœจ Add features on-the-fly
  • ๐Ÿš€ Combine multiple behaviors
  • ๐Ÿ›ก๏ธ Keep original code untouched

๐Ÿ’ก Why Use Decorator Pattern?

Hereโ€™s why developers love decorators:

  1. Open/Closed Principle ๐Ÿ”’: Classes open for extension, closed for modification
  2. Flexible Composition ๐Ÿ’ป: Mix and match behaviors like LEGO blocks
  3. Runtime Configuration ๐Ÿ“–: Add features based on user preferences
  4. Single Responsibility ๐Ÿ”ง: Each decorator does one thing well

Real-world example: Imagine building a coffee shop app โ˜•. With decorators, you can add milk, sugar, whipped cream, or caramel to any coffee without changing the base coffee class!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Simple Example

Letโ€™s start with a friendly example:

// ๐Ÿ‘‹ Hello, Decorator Pattern!
interface Coffee {
  cost(): number;
  description(): string;
}

// ๐ŸŽจ Base coffee implementation
class SimpleCoffee implements Coffee {
  cost(): number {
    return 2.00; // โ˜• Basic coffee price
  }
  
  description(): string {
    return "Simple coffee";
  }
}

// ๐ŸŽฏ Abstract decorator
abstract class CoffeeDecorator implements Coffee {
  constructor(protected coffee: Coffee) {}
  
  abstract cost(): number;
  abstract description(): string;
}

๐Ÿ’ก Explanation: We define a base interface and a simple implementation. The abstract decorator will be our foundation for adding features!

๐ŸŽฏ Common Patterns

Here are patterns youโ€™ll use daily:

// ๐Ÿ—๏ธ Pattern 1: Milk decorator
class MilkDecorator extends CoffeeDecorator {
  cost(): number {
    return this.coffee.cost() + 0.50; // ๐Ÿฅ› Add milk cost
  }
  
  description(): string {
    return `${this.coffee.description()} + Milk`;
  }
}

// ๐ŸŽจ Pattern 2: Sugar decorator
class SugarDecorator extends CoffeeDecorator {
  cost(): number {
    return this.coffee.cost() + 0.25; // ๐Ÿฌ Sweet addition
  }
  
  description(): string {
    return `${this.coffee.description()} + Sugar`;
  }
}

// ๐Ÿ”„ Pattern 3: Using decorators
const myCoffee = new SimpleCoffee();
const coffeeWithMilk = new MilkDecorator(myCoffee);
const sweetCoffee = new SugarDecorator(coffeeWithMilk);

console.log(`${sweetCoffee.description()} costs $${sweetCoffee.cost()}`);
// Output: Simple coffee + Milk + Sugar costs $2.75 โ˜•โœจ

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: E-commerce Product Features

Letโ€™s build something real:

// ๐Ÿ›๏ธ Define our product interface
interface Product {
  getPrice(): number;
  getDescription(): string;
  getFeatures(): string[];
}

// ๐Ÿ“ฆ Base product class
class BasicProduct implements Product {
  constructor(
    private name: string,
    private basePrice: number,
    private emoji: string // Every product needs an emoji! 
  ) {}
  
  getPrice(): number {
    return this.basePrice;
  }
  
  getDescription(): string {
    return `${this.emoji} ${this.name}`;
  }
  
  getFeatures(): string[] {
    return ["๐Ÿ“ฆ Standard packaging"];
  }
}

// ๐ŸŽ Gift wrap decorator
class GiftWrapDecorator implements Product {
  constructor(private product: Product) {}
  
  getPrice(): number {
    return this.product.getPrice() + 5.99; // ๐ŸŽ Gift wrap fee
  }
  
  getDescription(): string {
    return `${this.product.getDescription()} (Gift Wrapped)`;
  }
  
  getFeatures(): string[] {
    return [...this.product.getFeatures(), "๐ŸŽ Beautiful gift wrapping"];
  }
}

// โšก Express shipping decorator
class ExpressShippingDecorator implements Product {
  constructor(private product: Product) {}
  
  getPrice(): number {
    return this.product.getPrice() + 15.00; // ๐Ÿš€ Express fee
  }
  
  getDescription(): string {
    return `${this.product.getDescription()} with Express Shipping`;
  }
  
  getFeatures(): string[] {
    return [...this.product.getFeatures(), "โšก 24-hour delivery"];
  }
}

// ๐ŸŽฎ Let's use it!
const laptop = new BasicProduct("Gaming Laptop", 999.99, "๐Ÿ’ป");
const giftLaptop = new GiftWrapDecorator(laptop);
const rushOrder = new ExpressShippingDecorator(giftLaptop);

console.log(rushOrder.getDescription());
console.log(`Total: $${rushOrder.getPrice()}`);
console.log("Features:", rushOrder.getFeatures().join(", "));

๐ŸŽฏ Try it yourself: Add a warranty decorator and insurance decorator!

๐ŸŽฎ Example 2: Game Character Abilities

Letโ€™s make it fun:

// ๐Ÿ† Character interface
interface GameCharacter {
  attack(): number;
  defend(): number;
  getAbilities(): string[];
  getStats(): string;
}

// ๐Ÿฆธ Base character
class BaseCharacter implements GameCharacter {
  constructor(
    private name: string,
    private baseAttack: number,
    private baseDefense: number
  ) {}
  
  attack(): number {
    return this.baseAttack;
  }
  
  defend(): number {
    return this.baseDefense;
  }
  
  getAbilities(): string[] {
    return ["โš”๏ธ Basic Attack"];
  }
  
  getStats(): string {
    return `${this.name} - ATK: ${this.attack()} | DEF: ${this.defend()}`;
  }
}

// ๐Ÿ”ฅ Fire enchantment decorator
class FireEnchantment implements GameCharacter {
  constructor(private character: GameCharacter) {}
  
  attack(): number {
    return this.character.attack() + 15; // ๐Ÿ”ฅ Fire damage bonus
  }
  
  defend(): number {
    return this.character.defend();
  }
  
  getAbilities(): string[] {
    return [...this.character.getAbilities(), "๐Ÿ”ฅ Fire Strike"];
  }
  
  getStats(): string {
    return `${this.character.getStats()} ๐Ÿ”ฅ`;
  }
}

// ๐Ÿ›ก๏ธ Shield decorator
class ShieldDecorator implements GameCharacter {
  constructor(private character: GameCharacter) {}
  
  attack(): number {
    return this.character.attack();
  }
  
  defend(): number {
    return this.character.defend() + 20; // ๐Ÿ›ก๏ธ Shield bonus
  }
  
  getAbilities(): string[] {
    return [...this.character.getAbilities(), "๐Ÿ›ก๏ธ Shield Block"];
  }
  
  getStats(): string {
    return `${this.character.getStats()} ๐Ÿ›ก๏ธ`;
  }
}

// ๐ŸŽฎ Create a powerful character!
const hero = new BaseCharacter("Knight", 50, 30);
const fireKnight = new FireEnchantment(hero);
const shieldedFireKnight = new ShieldDecorator(fireKnight);

console.log(shieldedFireKnight.getStats());
console.log("Abilities:", shieldedFireKnight.getAbilities().join(", "));

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Advanced Topic 1: TypeScript Decorators

When youโ€™re ready to level up, try TypeScriptโ€™s built-in decorators:

// ๐ŸŽฏ Method decorator
const LogExecution = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
  const originalMethod = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    console.log(`โšก Executing ${propertyKey} with args:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`โœจ Result:`, result);
    return result;
  };
  
  return descriptor;
};

// ๐Ÿช„ Using the decorator
class MagicCalculator {
  @LogExecution
  multiply(a: number, b: number): number {
    return a * b;
  }
}

const calc = new MagicCalculator();
calc.multiply(5, 3); // โšก Logs execution automatically!

๐Ÿ—๏ธ Advanced Topic 2: Decorator Factory

For the brave developers:

// ๐Ÿš€ Decorator factory with configuration
const PowerUp = (powerLevel: number, emoji: string) => {
  return (constructor: Function) => {
    constructor.prototype.powerLevel = powerLevel;
    constructor.prototype.powerEmoji = emoji;
    constructor.prototype.showPower = function() {
      return `${this.powerEmoji} Power Level: ${this.powerLevel}`;
    };
  };
};

// ๐Ÿ’ช Apply decorators with different configs
@PowerUp(9000, "๐Ÿ”ฅ")
class SuperSaiyan {
  name = "Goku";
}

@PowerUp(100, "โšก")
class Pikachu {
  name = "Pikachu";
}

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Decorator Order Matters

// โŒ Wrong way - order confusion!
const coffee1 = new MilkDecorator(new SugarDecorator(new SimpleCoffee()));
const coffee2 = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));
// ๐Ÿ’ฅ Different results if decorators have side effects!

// โœ… Correct way - be intentional about order!
class OrderedDecorator {
  private decorators: CoffeeDecorator[] = [];
  
  add(decorator: typeof CoffeeDecorator, coffee: Coffee): Coffee {
    // ๐ŸŽฏ Clear, trackable decoration order
    return new decorator(coffee);
  }
}

๐Ÿคฏ Pitfall 2: Infinite Decoration

// โŒ Dangerous - might create infinite loop!
const addAllTheThings = (product: Product): Product => {
  return new GiftWrapDecorator(addAllTheThings(product));
  // ๐Ÿ’ฅ Stack overflow!
}

// โœ… Safe - add validation!
class SafeDecorator {
  private static maxDecorations = 5;
  private decorationCount = 0;
  
  decorate(product: Product): Product {
    if (this.decorationCount >= SafeDecorator.maxDecorations) {
      console.log("โš ๏ธ Maximum decorations reached!");
      return product;
    }
    this.decorationCount++;
    return new GiftWrapDecorator(product); // โœ… Safe now!
  }
}

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Keep Decorators Focused: Each decorator should add one specific feature
  2. ๐Ÿ“ Document Decoration Order: Make it clear which order decorators should be applied
  3. ๐Ÿ›ก๏ธ Validate Inputs: Check that the wrapped object is valid
  4. ๐ŸŽจ Use Meaningful Names: PremiumShippingDecorator not Decorator1
  5. โœจ Consider Performance: Too many decorators can impact performance

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Pizza Order System

Create a type-safe pizza ordering system with decorators:

๐Ÿ“‹ Requirements:

  • โœ… Base pizza with size options (small, medium, large)
  • ๐Ÿท๏ธ Topping decorators (cheese, pepperoni, mushrooms, etc.)
  • ๐Ÿ‘ค Special request decorator for custom instructions
  • ๐Ÿ“… Delivery time decorator
  • ๐ŸŽจ Each topping needs an emoji!

๐Ÿš€ Bonus Points:

  • Add pricing based on size
  • Implement a maximum toppings limit
  • Create a receipt generator

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
// ๐ŸŽฏ Our type-safe pizza system!
interface Pizza {
  getCost(): number;
  getDescription(): string;
  getToppings(): string[];
  getSize(): "small" | "medium" | "large";
}

// ๐Ÿ• Base pizza
class BasePizza implements Pizza {
  constructor(private size: "small" | "medium" | "large") {}
  
  getCost(): number {
    const prices = { small: 8, medium: 12, large: 16 };
    return prices[this.size];
  }
  
  getDescription(): string {
    return `${this.size} pizza`;
  }
  
  getToppings(): string[] {
    return ["๐Ÿ… Tomato sauce", "๐Ÿง€ Mozzarella"];
  }
  
  getSize(): "small" | "medium" | "large" {
    return this.size;
  }
}

// ๐Ÿ• Abstract topping decorator
abstract class ToppingDecorator implements Pizza {
  constructor(protected pizza: Pizza) {}
  
  abstract getCost(): number;
  abstract getDescription(): string;
  abstract getToppings(): string[];
  
  getSize(): "small" | "medium" | "large" {
    return this.pizza.getSize();
  }
}

// ๐Ÿฅ“ Pepperoni decorator
class PepperoniDecorator extends ToppingDecorator {
  getCost(): number {
    return this.pizza.getCost() + 2.50;
  }
  
  getDescription(): string {
    return `${this.pizza.getDescription()} + Pepperoni`;
  }
  
  getToppings(): string[] {
    return [...this.pizza.getToppings(), "๐Ÿฅ“ Pepperoni"];
  }
}

// ๐Ÿ„ Mushroom decorator
class MushroomDecorator extends ToppingDecorator {
  getCost(): number {
    return this.pizza.getCost() + 1.50;
  }
  
  getDescription(): string {
    return `${this.pizza.getDescription()} + Mushrooms`;
  }
  
  getToppings(): string[] {
    return [...this.pizza.getToppings(), "๐Ÿ„ Mushrooms"];
  }
}

// ๐Ÿ“ Special request decorator
class SpecialRequestDecorator extends ToppingDecorator {
  constructor(pizza: Pizza, private request: string) {
    super(pizza);
  }
  
  getCost(): number {
    return this.pizza.getCost(); // ๐ŸŽ No extra charge!
  }
  
  getDescription(): string {
    return `${this.pizza.getDescription()} (${this.request})`;
  }
  
  getToppings(): string[] {
    return [...this.pizza.getToppings(), `๐Ÿ“ Special: ${this.request}`];
  }
}

// ๐Ÿšš Delivery decorator
class DeliveryDecorator implements Pizza {
  constructor(
    private pizza: Pizza,
    private deliveryTime: number // Minutes
  ) {}
  
  getCost(): number {
    const deliveryFee = this.deliveryTime <= 30 ? 5 : 3;
    return this.pizza.getCost() + deliveryFee;
  }
  
  getDescription(): string {
    return `${this.pizza.getDescription()} ๐Ÿšš (${this.deliveryTime}min delivery)`;
  }
  
  getToppings(): string[] {
    return this.pizza.getToppings();
  }
  
  getSize(): "small" | "medium" | "large" {
    return this.pizza.getSize();
  }
}

// ๐ŸŽฎ Test it out!
const myPizza = new BasePizza("large");
const pepperoniPizza = new PepperoniDecorator(myPizza);
const mushroomPepperoniPizza = new MushroomDecorator(pepperoniPizza);
const specialPizza = new SpecialRequestDecorator(
  mushroomPepperoniPizza,
  "Extra crispy crust"
);
const deliveredPizza = new DeliveryDecorator(specialPizza, 25);

console.log("๐Ÿ• Order Summary:");
console.log(`Description: ${deliveredPizza.getDescription()}`);
console.log(`Toppings: ${deliveredPizza.getToppings().join(", ")}`);
console.log(`Total Cost: $${deliveredPizza.getCost().toFixed(2)}`);

๐ŸŽ“ Key Takeaways

Youโ€™ve learned so much! Hereโ€™s what you can now do:

  • โœ… Create decorators with confidence ๐Ÿ’ช
  • โœ… Avoid common mistakes like infinite loops and order issues ๐Ÿ›ก๏ธ
  • โœ… Apply decorators in real projects ๐ŸŽฏ
  • โœ… Debug decorator chains like a pro ๐Ÿ›
  • โœ… Build flexible systems with TypeScript! ๐Ÿš€

Remember: The Decorator Pattern is your friend for adding features without changing existing code! ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered the Decorator Pattern!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice with the pizza ordering exercise above
  2. ๐Ÿ—๏ธ Build a notification system with email, SMS, and push decorators
  3. ๐Ÿ“š Move on to our next tutorial: Abstract Factory Pattern
  4. ๐ŸŒŸ Share your decorated creations with others!

Remember: Every design pattern expert was once a beginner. Keep coding, keep learning, and most importantly, have fun! ๐Ÿš€


Happy coding! ๐ŸŽ‰๐Ÿš€โœจ