+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 301 of 355

๐Ÿ“˜ Observer Pattern: Event Subscription

Master observer pattern: event subscription 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

Ever wondered how YouTube knows to update your notification bell when your favorite creator uploads? Or how your weather app updates automatically when conditions change? Thatโ€™s the magic of the Observer Pattern in action! ๐ŸŒŸ

The Observer Pattern is like having a group chat where everyone gets notified when someone posts something important. Itโ€™s one of the most useful design patterns in TypeScript, and today youโ€™re going to master it! ๐Ÿ’ช

In this tutorial, youโ€™ll learn:

  • What the Observer Pattern is and why itโ€™s so powerful ๐Ÿ”ฎ
  • How to implement it with TypeScriptโ€™s type safety ๐Ÿ›ก๏ธ
  • Real-world examples thatโ€™ll make you go โ€œAha!โ€ ๐Ÿ’ก
  • Best practices to avoid common pitfalls ๐ŸŽฏ

Ready to become an event subscription wizard? Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Observer Pattern

The Observer Pattern is like subscribing to your favorite newsletter ๐Ÿ“ง. When new content is published (an event occurs), all subscribers (observers) automatically receive it without having to constantly check for updates!

Real-World Analogy ๐ŸŒ

Think of it like a pizza delivery tracking system ๐Ÿ•:

  • The Pizza Tracker is the Subject (what youโ€™re observing)
  • You and other customers are the Observers (subscribers)
  • When the pizza status changes, everyone gets notified! ๐Ÿ“ฑ

Benefits of Observer Pattern ๐ŸŽ

  • Loose Coupling: Subjects and observers donโ€™t need to know each otherโ€™s details ๐Ÿ”Œ
  • Dynamic Relationships: Add or remove observers at runtime ๐Ÿ”„
  • Event-Driven Architecture: Perfect for reactive applications โšก
  • Scalability: Easy to add new observer types without changing existing code ๐Ÿ“ˆ

๐Ÿ”ง Basic Syntax and Usage

Letโ€™s start with a simple Observer Pattern implementation in TypeScript:

// ๐Ÿ“ Define the Observer interface
interface Observer<T> {
  update(data: T): void;
}

// ๐Ÿ“ข Define the Subject interface
interface Subject<T> {
  attach(observer: Observer<T>): void;
  detach(observer: Observer<T>): void;
  notify(data: T): void;
}

// ๐ŸŽฏ Concrete Subject implementation
class EventEmitter<T> implements Subject<T> {
  private observers: Observer<T>[] = [];

  // โž• Add an observer
  attach(observer: Observer<T>): void {
    console.log('๐Ÿ‘‹ New observer joined!');
    this.observers.push(observer);
  }

  // โž– Remove an observer
  detach(observer: Observer<T>): void {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
      console.log('๐Ÿ‘‹ Observer left!');
    }
  }

  // ๐Ÿ“ข Notify all observers
  notify(data: T): void {
    console.log('๐Ÿ“ฃ Broadcasting to all observers...');
    this.observers.forEach(observer => observer.update(data));
  }
}

// ๐Ÿ‘€ Concrete Observer
class NewsSubscriber implements Observer<string> {
  constructor(private name: string) {}

  update(news: string): void {
    console.log(`๐Ÿ“ฐ ${this.name} received: ${news}`);
  }
}

// ๐Ÿš€ Let's see it in action!
const newsChannel = new EventEmitter<string>();

const alice = new NewsSubscriber('Alice');
const bob = new NewsSubscriber('Bob');

newsChannel.attach(alice);
newsChannel.attach(bob);

newsChannel.notify('Breaking: TypeScript 5.0 Released! ๐ŸŽ‰');
// Output:
// ๐Ÿ‘‹ New observer joined!
// ๐Ÿ‘‹ New observer joined!
// ๐Ÿ“ฃ Broadcasting to all observers...
// ๐Ÿ“ฐ Alice received: Breaking: TypeScript 5.0 Released! ๐ŸŽ‰
// ๐Ÿ“ฐ Bob received: Breaking: TypeScript 5.0 Released! ๐ŸŽ‰

๐Ÿ’ก Practical Examples

Example 1: Stock Price Tracker ๐Ÿ“ˆ

Letโ€™s build a stock price monitoring system where investors get notified of price changes:

// ๐Ÿ’ฐ Stock price update interface
interface StockUpdate {
  symbol: string;
  price: number;
  change: number;
  timestamp: Date;
}

// ๐Ÿ“Š Stock ticker subject
class StockTicker implements Subject<StockUpdate> {
  private observers: Observer<StockUpdate>[] = [];
  private stocks: Map<string, number> = new Map();

  attach(observer: Observer<StockUpdate>): void {
    this.observers.push(observer);
    console.log('๐Ÿ“ˆ New investor subscribed!');
  }

  detach(observer: Observer<StockUpdate>): void {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }

  notify(data: StockUpdate): void {
    this.observers.forEach(observer => observer.update(data));
  }

  // ๐Ÿ’น Update stock price
  updateStock(symbol: string, newPrice: number): void {
    const oldPrice = this.stocks.get(symbol) || newPrice;
    this.stocks.set(symbol, newPrice);
    
    const change = ((newPrice - oldPrice) / oldPrice) * 100;
    
    const update: StockUpdate = {
      symbol,
      price: newPrice,
      change,
      timestamp: new Date()
    };

    console.log(`๐Ÿ“Š ${symbol}: $${newPrice} (${change > 0 ? '+' : ''}${change.toFixed(2)}%)`);
    this.notify(update);
  }
}

// ๐Ÿ’ผ Different types of investors
class DayTrader implements Observer<StockUpdate> {
  constructor(private name: string) {}

  update(data: StockUpdate): void {
    if (Math.abs(data.change) > 2) {
      console.log(`๐Ÿƒ ${this.name}: Quick trade on ${data.symbol}! Big move: ${data.change.toFixed(2)}%`);
    }
  }
}

class LongTermInvestor implements Observer<StockUpdate> {
  constructor(private name: string) {}

  update(data: StockUpdate): void {
    if (data.change < -10) {
      console.log(`๐Ÿ’Ž ${this.name}: Buying the dip on ${data.symbol}! Down ${data.change.toFixed(2)}%`);
    }
  }
}

// ๐ŸŽฎ Let's simulate the market!
const nasdaq = new StockTicker();

const dayTrader = new DayTrader('FastMoney Mike');
const investor = new LongTermInvestor('Patient Patricia');

nasdaq.attach(dayTrader);
nasdaq.attach(investor);

// ๐Ÿ“ˆ Market simulation
nasdaq.updateStock('TSLA', 250.00);
nasdaq.updateStock('TSLA', 260.00);  // +4% triggers day trader
nasdaq.updateStock('AAPL', 180.00);
nasdaq.updateStock('AAPL', 160.00);  // -11% triggers long-term investor

Example 2: Game Achievement System ๐ŸŽฎ

Create an achievement notification system for a game:

// ๐Ÿ† Achievement data
interface Achievement {
  id: string;
  name: string;
  description: string;
  points: number;
  icon: string;
}

// ๐ŸŽฎ Player progress data
interface ProgressUpdate {
  playerId: string;
  metric: string;
  value: number;
}

// ๐Ÿ… Achievement manager
class AchievementManager implements Subject<Achievement> {
  private observers: Observer<Achievement>[] = [];
  private playerProgress: Map<string, Map<string, number>> = new Map();
  
  private achievements: Achievement[] = [
    { id: 'first_kill', name: 'First Blood', description: 'Defeat your first enemy', points: 10, icon: 'โš”๏ธ' },
    { id: 'speed_demon', name: 'Speed Demon', description: 'Complete level in under 60 seconds', points: 25, icon: 'โšก' },
    { id: 'collector', name: 'Collector', description: 'Collect 100 coins', points: 50, icon: '๐Ÿ’ฐ' }
  ];

  attach(observer: Observer<Achievement>): void {
    this.observers.push(observer);
  }

  detach(observer: Observer<Achievement>): void {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }

  notify(achievement: Achievement): void {
    this.observers.forEach(observer => observer.update(achievement));
  }

  // ๐Ÿ“Š Update player progress
  updateProgress(update: ProgressUpdate): void {
    if (!this.playerProgress.has(update.playerId)) {
      this.playerProgress.set(update.playerId, new Map());
    }
    
    const progress = this.playerProgress.get(update.playerId)!;
    progress.set(update.metric, update.value);

    // ๐ŸŽฏ Check for achievements
    this.checkAchievements(update.playerId, update.metric, update.value);
  }

  private checkAchievements(playerId: string, metric: string, value: number): void {
    // ๐Ÿ† Check each achievement condition
    if (metric === 'enemies_defeated' && value === 1) {
      this.unlockAchievement('first_kill');
    } else if (metric === 'level_time' && value < 60) {
      this.unlockAchievement('speed_demon');
    } else if (metric === 'coins_collected' && value >= 100) {
      this.unlockAchievement('collector');
    }
  }

  private unlockAchievement(achievementId: string): void {
    const achievement = this.achievements.find(a => a.id === achievementId);
    if (achievement) {
      console.log(`๐ŸŽŠ Achievement Unlocked: ${achievement.icon} ${achievement.name}!`);
      this.notify(achievement);
    }
  }
}

// ๐Ÿ“ฑ UI notification system
class NotificationUI implements Observer<Achievement> {
  update(achievement: Achievement): void {
    console.log(`๐ŸŽฏ UI Popup: ${achievement.icon} ${achievement.name} - ${achievement.points} points!`);
  }
}

// ๐Ÿ“Š Stats tracker
class PlayerStats implements Observer<Achievement> {
  private totalPoints = 0;

  update(achievement: Achievement): void {
    this.totalPoints += achievement.points;
    console.log(`๐Ÿ“ˆ Stats Updated: Total points = ${this.totalPoints}`);
  }
}

// ๐ŸŽฎ Game in action!
const gameAchievements = new AchievementManager();
const ui = new NotificationUI();
const stats = new PlayerStats();

gameAchievements.attach(ui);
gameAchievements.attach(stats);

// ๐ŸŽฏ Player actions
gameAchievements.updateProgress({ playerId: 'player1', metric: 'enemies_defeated', value: 1 });
gameAchievements.updateProgress({ playerId: 'player1', metric: 'level_time', value: 45 });
gameAchievements.updateProgress({ playerId: 'player1', metric: 'coins_collected', value: 100 });

Example 3: Shopping Cart with Real-time Updates ๐Ÿ›’

// ๐Ÿ›๏ธ Shopping cart events
type CartEvent = 'item_added' | 'item_removed' | 'cart_cleared';

interface CartUpdate {
  event: CartEvent;
  item?: Product;
  totalItems: number;
  totalPrice: number;
}

interface Product {
  id: string;
  name: string;
  price: number;
  emoji: string;
}

// ๐Ÿ›’ Shopping cart subject
class ShoppingCart implements Subject<CartUpdate> {
  private observers: Observer<CartUpdate>[] = [];
  private items: Map<string, { product: Product; quantity: number }> = new Map();

  attach(observer: Observer<CartUpdate>): void {
    this.observers.push(observer);
  }

  detach(observer: Observer<CartUpdate>): void {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }

  notify(update: CartUpdate): void {
    this.observers.forEach(observer => observer.update(update));
  }

  // โž• Add item to cart
  addItem(product: Product, quantity: number = 1): void {
    const existing = this.items.get(product.id);
    if (existing) {
      existing.quantity += quantity;
    } else {
      this.items.set(product.id, { product, quantity });
    }

    console.log(`${product.emoji} Added ${product.name} to cart!`);
    this.notify({
      event: 'item_added',
      item: product,
      totalItems: this.getTotalItems(),
      totalPrice: this.getTotalPrice()
    });
  }

  // โž– Remove item from cart
  removeItem(productId: string): void {
    const item = this.items.get(productId);
    if (item) {
      this.items.delete(productId);
      console.log(`${item.product.emoji} Removed ${item.product.name} from cart!`);
      this.notify({
        event: 'item_removed',
        item: item.product,
        totalItems: this.getTotalItems(),
        totalPrice: this.getTotalPrice()
      });
    }
  }

  private getTotalItems(): number {
    return Array.from(this.items.values()).reduce((sum, item) => sum + item.quantity, 0);
  }

  private getTotalPrice(): number {
    return Array.from(this.items.values()).reduce(
      (sum, item) => sum + (item.product.price * item.quantity), 
      0
    );
  }
}

// ๐Ÿ’ฐ Price display observer
class PriceDisplay implements Observer<CartUpdate> {
  update(data: CartUpdate): void {
    console.log(`๐Ÿ’ฐ Total: $${data.totalPrice.toFixed(2)} (${data.totalItems} items)`);
  }
}

// ๐Ÿ“ง Email notification observer
class EmailNotifier implements Observer<CartUpdate> {
  update(data: CartUpdate): void {
    if (data.event === 'item_added' && data.totalPrice > 100) {
      console.log(`๐Ÿ“ง Email: You qualify for free shipping! ๐ŸŽ‰`);
    }
  }
}

// ๐Ÿ“Š Analytics observer
class Analytics implements Observer<CartUpdate> {
  private events: CartEvent[] = [];

  update(data: CartUpdate): void {
    this.events.push(data.event);
    console.log(`๐Ÿ“Š Analytics: ${data.event} tracked (Total events: ${this.events.length})`);
  }
}

// ๐Ÿ›๏ธ Shopping time!
const cart = new ShoppingCart();
const priceDisplay = new PriceDisplay();
const emailer = new EmailNotifier();
const analytics = new Analytics();

cart.attach(priceDisplay);
cart.attach(emailer);
cart.attach(analytics);

// ๐Ÿ›’ Add some products
const products: Product[] = [
  { id: '1', name: 'Gaming Keyboard', price: 79.99, emoji: 'โŒจ๏ธ' },
  { id: '2', name: 'Gaming Mouse', price: 49.99, emoji: '๐Ÿ–ฑ๏ธ' },
  { id: '3', name: 'Monitor', price: 299.99, emoji: '๐Ÿ–ฅ๏ธ' }
];

cart.addItem(products[0]);
cart.addItem(products[1]);
cart.addItem(products[2]); // Triggers free shipping email!

๐Ÿš€ Advanced Concepts

Type-Safe Event Emitter with Generics ๐Ÿงฌ

Letโ€™s create a more advanced, type-safe event system:

// ๐ŸŽฏ Event map for strong typing
interface GameEvents {
  'player:levelUp': { playerId: string; newLevel: number; };
  'player:scoreUpdate': { playerId: string; score: number; highScore: boolean; };
  'game:over': { winner: string; finalScore: number; };
}

// ๐Ÿ”ฅ Advanced type-safe event emitter
class TypedEventEmitter<TEvents extends Record<string, any>> {
  private listeners: {
    [K in keyof TEvents]?: Array<(data: TEvents[K]) => void>;
  } = {};

  // ๐Ÿ‘‚ Type-safe event listener
  on<K extends keyof TEvents>(event: K, listener: (data: TEvents[K]) => void): () => void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    
    this.listeners[event]!.push(listener);
    
    // ๐Ÿ”Œ Return unsubscribe function
    return () => this.off(event, listener);
  }

  // ๐Ÿ”‡ Remove listener
  off<K extends keyof TEvents>(event: K, listener: (data: TEvents[K]) => void): void {
    const listeners = this.listeners[event];
    if (listeners) {
      const index = listeners.indexOf(listener);
      if (index > -1) {
        listeners.splice(index, 1);
      }
    }
  }

  // ๐Ÿ“ข Emit type-safe event
  emit<K extends keyof TEvents>(event: K, data: TEvents[K]): void {
    const listeners = this.listeners[event];
    if (listeners) {
      listeners.forEach(listener => listener(data));
    }
  }

  // ๐ŸŽฏ Once listener (auto-unsubscribe after first call)
  once<K extends keyof TEvents>(event: K, listener: (data: TEvents[K]) => void): void {
    const unsubscribe = this.on(event, (data) => {
      listener(data);
      unsubscribe();
    });
  }
}

// ๐ŸŽฎ Game system using typed events
class GameSystem {
  private events = new TypedEventEmitter<GameEvents>();
  
  // Expose event methods
  on = this.events.on.bind(this.events);
  emit = this.events.emit.bind(this.events);
  
  // ๐ŸŽฏ Game actions
  levelUpPlayer(playerId: string, newLevel: number): void {
    console.log(`โฌ†๏ธ Player ${playerId} reached level ${newLevel}!`);
    this.emit('player:levelUp', { playerId, newLevel });
  }
  
  updateScore(playerId: string, score: number, highScore: boolean): void {
    console.log(`๐Ÿ“Š Player ${playerId} score: ${score}${highScore ? ' ๐Ÿ† NEW HIGH SCORE!' : ''}`);
    this.emit('player:scoreUpdate', { playerId, score, highScore });
  }
  
  endGame(winner: string, finalScore: number): void {
    console.log(`๐Ÿ Game Over! Winner: ${winner} with ${finalScore} points!`);
    this.emit('game:over', { winner, finalScore });
  }
}

// ๐ŸŽฏ Usage with full type safety!
const game = new GameSystem();

// โœ… TypeScript knows the exact shape of each event
game.on('player:levelUp', ({ playerId, newLevel }) => {
  console.log(`๐ŸŽŠ Celebration animation for ${playerId} reaching level ${newLevel}!`);
});

game.on('player:scoreUpdate', ({ score, highScore }) => {
  if (highScore) {
    console.log(`๐ŸŽ† Fireworks! New high score: ${score}!`);
  }
});

// ๐ŸŽฎ Simulate game
game.levelUpPlayer('Alice', 5);
game.updateScore('Alice', 5000, true);
game.endGame('Alice', 5000);

Async Observer Pattern ๐Ÿ”„

Handle asynchronous operations in observers:

// ๐Ÿ”„ Async observer interface
interface AsyncObserver<T> {
  update(data: T): Promise<void>;
}

// ๐Ÿ“ก Async subject
class AsyncEventEmitter<T> implements Subject<AsyncObserver<T>> {
  private observers: AsyncObserver<T>[] = [];

  attach(observer: AsyncObserver<T>): void {
    this.observers.push(observer);
  }

  detach(observer: AsyncObserver<T>): void {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }

  // ๐Ÿš€ Notify all observers asynchronously
  async notify(data: T): Promise<void> {
    console.log('๐Ÿ“ก Broadcasting async event...');
    
    // Option 1: Parallel execution
    await Promise.all(
      this.observers.map(observer => observer.update(data))
    );
    
    // Option 2: Sequential execution (uncomment if needed)
    // for (const observer of this.observers) {
    //   await observer.update(data);
    // }
  }
}

// ๐Ÿ’พ Database logger (async)
class DatabaseLogger implements AsyncObserver<string> {
  async update(message: string): Promise<void> {
    console.log('๐Ÿ’พ Saving to database...');
    // Simulate async database operation
    await new Promise(resolve => setTimeout(resolve, 100));
    console.log(`โœ… Logged: "${message}" to database`);
  }
}

// ๐Ÿ“ง Email sender (async)
class EmailSender implements AsyncObserver<string> {
  async update(message: string): Promise<void> {
    console.log('๐Ÿ“ง Sending email...');
    // Simulate async email sending
    await new Promise(resolve => setTimeout(resolve, 200));
    console.log(`โœ… Email sent: "${message}"`);
  }
}

// ๐Ÿš€ Test async observers
async function testAsyncObservers() {
  const notifier = new AsyncEventEmitter<string>();
  
  notifier.attach(new DatabaseLogger());
  notifier.attach(new EmailSender());
  
  await notifier.notify('Important system event occurred! ๐Ÿšจ');
  console.log('๐ŸŽฏ All observers notified!');
}

// Run the test
testAsyncObservers();

โš ๏ธ Common Pitfalls and Solutions

โŒ Wrong: Memory Leaks from Forgotten Observers

// โŒ BAD: Observers never get removed
class LeakyComponent {
  private eventEmitter: EventEmitter<string>;
  
  constructor(emitter: EventEmitter<string>) {
    this.eventEmitter = emitter;
    // Observer attached but never detached!
    this.eventEmitter.attach({
      update: (data) => console.log(data)
    });
  }
  
  // No cleanup method! ๐Ÿ˜ฑ
}

โœ… Correct: Proper Observer Cleanup

// โœ… GOOD: Always clean up observers
class CleanComponent {
  private eventEmitter: EventEmitter<string>;
  private observer: Observer<string>;
  
  constructor(emitter: EventEmitter<string>) {
    this.eventEmitter = emitter;
    this.observer = {
      update: (data) => console.log(data)
    };
    this.eventEmitter.attach(this.observer);
  }
  
  // ๐Ÿงน Cleanup method
  destroy(): void {
    this.eventEmitter.detach(this.observer);
    console.log('๐Ÿงน Observer cleaned up!');
  }
}

โŒ Wrong: Modifying Observer List During Notification

// โŒ BAD: Causes unexpected behavior
class BadSubject<T> implements Subject<T> {
  private observers: Observer<T>[] = [];
  
  notify(data: T): void {
    // โŒ Direct iteration while list might change
    this.observers.forEach(observer => {
      observer.update(data);
      // What if update() calls detach()? ๐Ÿ’ฅ
    });
  }
}

โœ… Correct: Safe Notification with Copy

// โœ… GOOD: Iterate over a copy
class SafeSubject<T> implements Subject<T> {
  private observers: Observer<T>[] = [];
  
  notify(data: T): void {
    // โœ… Create a copy to iterate safely
    const observersCopy = [...this.observers];
    observersCopy.forEach(observer => {
      observer.update(data);
    });
  }
}

โŒ Wrong: Tight Coupling with Concrete Types

// โŒ BAD: Tightly coupled to specific implementation
class NewsChannel {
  private subscribers: EmailSubscriber[] = []; // โŒ Too specific!
  
  addSubscriber(subscriber: EmailSubscriber): void {
    this.subscribers.push(subscriber);
  }
}

โœ… Correct: Program to Interfaces

// โœ… GOOD: Loosely coupled with interfaces
class NewsChannel {
  private subscribers: Observer<string>[] = []; // โœ… Uses interface!
  
  addSubscriber(subscriber: Observer<string>): void {
    this.subscribers.push(subscriber);
  }
}

๐Ÿ› ๏ธ Best Practices

  1. ๐Ÿ” Use WeakMap for Private Observer Storage

    const observersMap = new WeakMap<Subject<any>, Observer<any>[]>();
  2. ๐ŸŽฏ Implement Unsubscribe Tokens

    const unsubscribe = emitter.on('event', handler);
    // Later: unsubscribe();
  3. ๐Ÿ“‹ Use Event Types for Better Organization

    type EventMap = { click: MouseEvent; change: string; };
  4. ๐Ÿ›ก๏ธ Add Error Handling in Observers

    try {
      observer.update(data);
    } catch (error) {
      console.error('Observer error:', error);
    }
  5. ๐Ÿ“Š Consider Using Existing Libraries

    • RxJS for complex reactive programming
    • Node.js EventEmitter for simple cases
    • MobX for state management with observers

๐Ÿงช Hands-On Exercise

Ready to put your skills to the test? Letโ€™s build a Chat Room system! ๐Ÿ’ฌ

Requirements:

  1. Create a ChatRoom class that acts as the subject
  2. Create different types of observers:
    • User - receives and displays messages
    • MessageLogger - logs all messages
    • ProfanityFilter - alerts on inappropriate content
  3. Messages should include: sender, content, timestamp
  4. Users should be able to join/leave the chat
  5. Bonus: Add private messaging between users! ๐Ÿคซ
๐Ÿ’ก Click here for the solution
// ๐Ÿ’ฌ Message interface
interface ChatMessage {
  id: string;
  sender: string;
  content: string;
  timestamp: Date;
  isPrivate: boolean;
  recipient?: string;
}

// ๐Ÿ  Chat room subject
class ChatRoom implements Subject<ChatMessage> {
  private observers: Map<string, Observer<ChatMessage>> = new Map();
  private messageHistory: ChatMessage[] = [];
  private messageId = 0;

  // ๐Ÿ‘‹ User joins chat
  join(username: string, observer: Observer<ChatMessage>): void {
    this.observers.set(username, observer);
    console.log(`๐Ÿ‘‹ ${username} joined the chat!`);
    
    // Send join notification
    this.broadcast('System', `${username} has entered the chat! ๐ŸŽ‰`);
  }

  // ๐Ÿ‘‹ User leaves chat
  leave(username: string): void {
    if (this.observers.delete(username)) {
      console.log(`๐Ÿ‘‹ ${username} left the chat!`);
      this.broadcast('System', `${username} has left the chat ๐Ÿ˜ข`);
    }
  }

  // ๐Ÿ“ข Public message
  sendMessage(sender: string, content: string): void {
    const message: ChatMessage = {
      id: `msg-${++this.messageId}`,
      sender,
      content,
      timestamp: new Date(),
      isPrivate: false
    };
    
    this.messageHistory.push(message);
    this.notify(message);
  }

  // ๐Ÿคซ Private message
  sendPrivateMessage(sender: string, recipient: string, content: string): void {
    const message: ChatMessage = {
      id: `msg-${++this.messageId}`,
      sender,
      content,
      timestamp: new Date(),
      isPrivate: true,
      recipient
    };
    
    this.messageHistory.push(message);
    
    // Only notify sender and recipient
    const senderObserver = this.observers.get(sender);
    const recipientObserver = this.observers.get(recipient);
    
    if (senderObserver) senderObserver.update(message);
    if (recipientObserver) recipientObserver.update(message);
    
    console.log(`๐Ÿคซ Private message from ${sender} to ${recipient}`);
  }

  // ๐Ÿ“ฃ System broadcast
  private broadcast(sender: string, content: string): void {
    const message: ChatMessage = {
      id: `msg-${++this.messageId}`,
      sender,
      content,
      timestamp: new Date(),
      isPrivate: false
    };
    
    this.notify(message);
  }

  // Required Subject methods
  attach(observer: Observer<ChatMessage>): void {
    // Not used directly - use join() instead
  }

  detach(observer: Observer<ChatMessage>): void {
    // Not used directly - use leave() instead
  }

  notify(message: ChatMessage): void {
    if (message.isPrivate) return; // Private messages handled separately
    
    this.observers.forEach((observer, username) => {
      observer.update(message);
    });
  }
}

// ๐Ÿ‘ค Chat user
class User implements Observer<ChatMessage> {
  constructor(private username: string) {}

  update(message: ChatMessage): void {
    const time = message.timestamp.toLocaleTimeString();
    
    if (message.isPrivate) {
      if (message.sender === this.username) {
        console.log(`๐Ÿ“ค [${time}] You โ†’ ${message.recipient}: ${message.content}`);
      } else {
        console.log(`๐Ÿ“จ [${time}] ${message.sender} โ†’ You (private): ${message.content}`);
      }
    } else {
      const prefix = message.sender === 'System' ? '๐Ÿ“ข' : '๐Ÿ’ฌ';
      console.log(`${prefix} [${time}] ${message.sender}: ${message.content}`);
    }
  }
}

// ๐Ÿ“ Message logger
class MessageLogger implements Observer<ChatMessage> {
  private logFile: ChatMessage[] = [];

  update(message: ChatMessage): void {
    this.logFile.push(message);
    console.log(`๐Ÿ“ Logged message #${this.logFile.length}`);
  }
}

// ๐Ÿšซ Profanity filter
class ProfanityFilter implements Observer<ChatMessage> {
  private badWords = ['spam', 'scam', 'fake'];

  update(message: ChatMessage): void {
    const hasProfanity = this.badWords.some(word => 
      message.content.toLowerCase().includes(word)
    );
    
    if (hasProfanity) {
      console.log(`๐Ÿšจ Warning: Inappropriate content detected from ${message.sender}!`);
    }
  }
}

// ๐ŸŽฎ Let's chat!
const chatRoom = new ChatRoom();
const logger = new MessageLogger();
const filter = new ProfanityFilter();

// Set up system observers
chatRoom.attach(logger);
chatRoom.attach(filter);

// Users join
const alice = new User('Alice');
const bob = new User('Bob');
const charlie = new User('Charlie');

chatRoom.join('Alice', alice);
chatRoom.join('Bob', bob);
chatRoom.join('Charlie', charlie);

// Chat simulation
chatRoom.sendMessage('Alice', 'Hey everyone! ๐Ÿ‘‹');
chatRoom.sendMessage('Bob', 'Hi Alice! How are you?');
chatRoom.sendPrivateMessage('Alice', 'Bob', "I'm great! Want to grab coffee later? โ˜•");
chatRoom.sendMessage('Charlie', 'This chat is spam!'); // Triggers profanity filter
chatRoom.leave('Charlie');

๐ŸŽ“ Key Takeaways

Youโ€™ve mastered the Observer Pattern! Hereโ€™s what youโ€™ve learned:

โœ… Observer Pattern Basics: Subjects notify observers of state changes automatically
โœ… Type-Safe Implementation: Use TypeScript interfaces and generics for robust code
โœ… Real-World Applications: Stock tickers, game systems, shopping carts, and more
โœ… Memory Management: Always detach observers to prevent memory leaks
โœ… Advanced Patterns: Async observers, typed event emitters, and error handling

Remember: The Observer Pattern is perfect when you need loose coupling between objects that need to communicate! ๐Ÿ”Œ

๐Ÿค Next Steps

Congratulations on mastering the Observer Pattern! ๐ŸŽ‰ Youโ€™re becoming a true TypeScript design pattern expert!

Hereโ€™s what to explore next:

  1. ๐Ÿ“š Try implementing Observer Pattern with RxJS for reactive programming
  2. ๐Ÿ”„ Combine Observer with other patterns like Command or Mediator
  3. ๐Ÿ—๏ธ Build a real-time notification system for your next project
  4. ๐Ÿš€ Check out the next tutorial: Strategy Pattern: Flexible Algorithms

The Observer Pattern is everywhere in modern applications - from Reactโ€™s state management to Node.js EventEmitters. Now you have the power to use it effectively!

Keep building amazing event-driven systems! Youโ€™re doing fantastic! ๐Ÿ’ชโœจ