Observer Pattern in TypeScript

đź‘€ Views 12.5K
🔄 Shares 847
⏰ Read time 7 min

The Observer Pattern is a fundamental design pattern in software engineering that establishes a one-to-many relationship between objects. When one object (the subject) changes state, all its dependents (observers) are notified and updated automatically. This pattern is widely used in UI frameworks, event handling systems, and distributed computing to create reactive and loosely coupled components.

In TypeScript, the Observer Pattern becomes even more powerful due to its strong typing system, which helps prevent runtime errors and improves code maintainability. Let’s explore how to implement this pattern effectively in TypeScript with real-world examples and best practices.

🔹 Why It Matters

The Observer Pattern is crucial for:

  • Decoupling components: Subjects don’t need to know about their observers directly
  • Dynamic relationships: Observers can be added or removed at runtime
  • Event-driven architectures: Natural fit for handling user interactions, sensor data, etc.
  • Scalability: Easily add new observers without modifying existing code

Common use cases include:

  • GUI event handling (clicks, mouse movements)
  • Real-time data updates (stock tickers, chat applications)
  • Model-View separation (MVC/MVVM patterns)
  • Pub/Sub messaging systems

🔹 Core Concepts

The Observer Pattern consists of three main components:

  1. Subject (Observable): Maintains a list of observers and provides methods to attach/detach them
  2. Observer (Subscriber): Defines an update interface that gets called when the subject changes
  3. Concrete Subject: The actual object being observed (e.g., a weather station)
  4. Concrete Observer: Implements the update interface for specific behavior

Observer Pattern UML Diagram
Source: Wikimedia Commons (Creative Commons Attribution-Share Alike 3.0 Unported license)

🔹 Code Walkthrough

Let’s implement a simple weather station system where multiple displays update when temperature changes:

// 1. Define the Observer interface
interface WeatherObserver {
    update(temperature: number, humidity: number): void;
}

// 2. Define the Subject interface
interface WeatherSubject {
    registerObserver(observer: WeatherObserver): void;
    removeObserver(observer: WeatherObserver): void;
    notifyObservers(): void;
}

// 3. Concrete Subject - WeatherStation
class WeatherStation implements WeatherSubject {
    private observers: WeatherObserver[] = [];
    private temperature: number = 0;
    private humidity: number = 0;

    registerObserver(observer: WeatherObserver): void {
        this.observers.push(observer);
    }

    removeObserver(observer: WeatherObserver): void {
        const index = this.observers.indexOf(observer);
        if (index > -1) {
            this.observers.splice(index, 1);
        }
    }

    notifyObservers(): void {
        for (const observer of this.observers) {
            observer.update(this.temperature, this.humidity);
        }
    }

    // Business logic methods
    setMeasurements(temperature: number, humidity: number): void {
        this.temperature = temperature;
        this.humidity = humidity;
        this.notifyObservers(); // Notify when data changes
    }
}

// 4. Concrete Observers
class PhoneDisplay implements WeatherObserver {
    update(temperature: number, humidity: number): void {
        console.log(`Phone Display: ${temperature}°C and ${humidity}% humidity`);
    }
}

class TVDisplay implements WeatherObserver {
    update(temperature: number, humidity: number): void {
        console.log(`TV Display: Current weather - Temp: ${temperature}°C, Humidity: ${humidity}%`);
    }
}

// Usage
const weatherStation = new WeatherStation();

const phoneDisplay = new PhoneDisplay();
const tvDisplay = new TVDisplay();

weatherStation.registerObserver(phoneDisplay);
weatherStation.registerObserver(tvDisplay);

weatherStation.setMeasurements(25, 60); // Both displays update
/* Output:
Phone Display: 25°C and 60% humidity
TV Display: Current weather - Temp: 25°C, Humidity: 60%
*/

weatherStation.removeObserver(tvDisplay);
weatherStation.setMeasurements(26, 65); // Only phone display updates
/* Output:
Phone Display: 26°C and 65% humidity
*/

TypeScript-Specific Enhancements

We can leverage TypeScript features for better type safety:

// Using generics for stronger typing
class TypedWeatherStation<T extends WeatherObserver> implements WeatherSubject {
    private observers: Set<T> = new Set(); // Using Set to avoid duplicates

    registerObserver(observer: T): void {
        this.observers.add(observer);
    }

    removeObserver(observer: T): void {
        this.observers.delete(observer);
    }

    notifyObservers(): void {
        this.observers.forEach(observer => {
            // TypeScript knows observer has update method
            observer.update(this.temperature, this.humidity);
        });
    }
    // ... rest of implementation
}

🔹 Common Mistakes

  1. Memory leaks: Forgetting to remove observers when they’re no longer needed

    • Solution: Implement proper cleanup in component unmount phases
  2. Notification storms: Triggering too many updates in quick succession

    • Solution: Implement throttling/debouncing or batch updates
  3. Tight coupling: Subject knowing too much about observers

    • Solution: Keep the update interface minimal (just the data needed)
  4. Error handling: One observer failing shouldn’t break others

    • Solution: Wrap observer calls in try-catch blocks

🔹 Best Practices

  1. Use interfaces: Define clear contracts for both subjects and observers
  2. Consider immutability: When notifying, pass new data rather than modifying shared state
  3. Implement unsubscribe: Always provide a way to stop receiving notifications
  4. Use existing libraries: For complex apps, consider RxJS or EventEmitter
  5. Document notification order: If order matters, specify it in documentation

Advanced Implementation with RxJS

For more complex scenarios, RxJS provides powerful observer pattern implementation:

import { Observable, Observer } from 'rxjs';

// Create an observable
const weatherObservable = new Observable<{temp: number, humidity: number}>(subscriber => {
    // Simulate data updates
    const interval = setInterval(() => {
        const temp = Math.floor(Math.random() * 30) + 15;
        const humidity = Math.floor(Math.random() * 50) + 30;
        subscriber.next({ temp, humidity });
    }, 2000);

    // Cleanup
    return () => clearInterval(interval);
});

// Create observers
const phoneObserver: Observer<any> = {
    next: (data) => console.log('Phone:', data),
    error: (err) => console.error('Error:', err),
    complete: () => console.log('Completed')
};

const tvObserver: Observer<any> = {
    next: (data) => console.log('TV:', `Temp: ${data.temp}°C, Humidity: ${data.humidity}%`),
    error: (err) => console.error('Error:', err),
    complete: () => console.log('Completed')
};

// Subscribe
const phoneSub = weatherObservable.subscribe(phoneObserver);
const tvSub = weatherObservable.subscribe(tvObserver);

// Unsubscribe after 10 seconds
setTimeout(() => {
    phoneSub.unsubscribe();
    tvSub.unsubscribe();
}, 10000);

🔹 Final Thoughts

The Observer Pattern is a powerful tool for building reactive systems in TypeScript. By understanding its core principles and applying TypeScript’s type safety features, you can create maintainable, decoupled components that respond dynamically to changes.

For most applications, starting with the basic implementation shown earlier is sufficient. As your needs grow more complex, consider exploring:

  • RxJS for advanced reactive programming
  • Node.js EventEmitter for server-side applications
  • State management libraries like Redux or Vuex that build on observer concepts

Remember that the key benefit of this pattern is loose coupling - design your subjects and observers to minimize dependencies between them, and your system will be more flexible and easier to maintain.


Observer PatternDesign PatternsReactive ProgrammingEvent HandlingTypeScript TutorialTypeScript Examples

Related Articles

Command Pattern in TypeScript

Command Pattern in TypeScript

Learn the Command Pattern in TypeScript with real-world examples, code walkthroughs, and best practices for decoupling actions in your applications.

Command PatternTypeScriptDesign PatternsSoftware Engineering
Abstract Factory Pattern in TypeScript

Abstract Factory Pattern in TypeScript

Master the Abstract Factory Pattern in TypeScript with clear explanations, real-world examples, and best practices. Learn how to create families of related objects efficiently.

Abstract Factory Pattern Design Patterns Object Creation TypeScript Tutorial
Observer Pattern in TypeScript

Observer Pattern in TypeScript

Master the Observer Pattern in TypeScript with clear explanations, real-world examples, and best practices for building reactive applications.

Observer PatternDesign PatternsReactive ProgrammingEvent Handling
Strategy Pattern in TypeScript

Strategy Pattern in TypeScript

Learn the Strategy Pattern in TypeScript with real-world examples, code walkthroughs, and best practices.

Strategy Pattern TypeScript Design Patterns OOP
Factory Pattern in TypeScript

Factory Pattern in TypeScript

Learn how to implement the Factory Pattern in TypeScript to create flexible and scalable applications. This guide includes real-world examples, common pitfalls, and best practices.

Factory Pattern Design Patterns Object Creation Software Engineering
Load more articles