Create your own
Lesson illustration

Implementing a Generic Observer Interface

Hello! Welcome to the first lesson in our "Core Observable Architecture" module.

In the previous module, we successfully set up a robust project environment with TypeScript, Vite, BiomeJS, and Vitest. With our tooling in place, we're now ready to dive into the heart of our reactive library.

This lesson marks our first step in building the library's core logic. Our goal is to design and implement a generic Observer interface with next, error, and complete methods. The Observer is a fundamental concept in reactive programming; it's the object that "listens" to and consumes the data, errors, and completion signals emitted by a data stream. By the end of this lesson, you'll have a clear understanding of the Observer's role and a concrete TypeScript implementation ready for use in our project.

1. The Core Concepts: Observables, Observers, and Subscriptions

Before we write any code, it's essential to understand the roles of the three key players in a reactive system:

  • Observable: The source of data. It represents a stream of values that can be delivered over time. Think of it as the "producer" or "publisher".
  • Observer: The consumer of the data. It's an object with a set of callback methods that react to the values, errors, or completion signals sent by the Observable. It is the "consumer" or "subscriber".
  • Subscription: The link between an Observable and an Observer. It represents the execution of an Observable and is primarily used to cancel that execution (i.e., to unsubscribe).

To get a clear visual and conceptual overview of how these three components interact, let's start with a short video.

OBSERVABLES, OBSERVERS & SUBSCRIPTIONS | RxJS TUTORIAL

This video from the Academind channel provides an excellent introduction to the core concepts of RxJS, which we are using as a model for our library. It clearly explains the responsibilities of the Observer and its three critical methods.

Please watch the first two sections of the video — starting from the very beginning, core concepts, then continuing around the 2:41 mark with the stream analogy. Focus on how the narrator describes the relationship between the Observable and the Observer, and the specific purpose of the next, error, and complete methods.

As the video explains, the Observer provides a standardized "contract" for handling any notification an Observable might produce. This separation of concerns is what makes the pattern so powerful and flexible.

2. Designing the Observer Interface

Now that we have the concept down, let's translate it into a formal TypeScript interface. An interface is the perfect tool for defining a contract in TypeScript. It specifies the shape an object must have without dictating its implementation.

The article "Demystifying RxJS, Part I" provides a concise definition of the Observer that aligns perfectly with our goal.

Demystifying RxJS, Part I: Building our own Observables

This article by Travis Kaufman walks through building a simplified version of RxJS, which is exactly what we're doing. This section clearly defines the Observer and its methods.

In the 'Observers' section (which begins after the introductory example and runs up to the 'Subscriptions' section), read through the full section. Pay special attention to three core methods — the bullet list describing next(), error(), and complete() — and to the note about the distinction between 'Observer' and 'Subscriber' in the full RxJS library: Observer vs Subscriber.

Based on this, we can define our Observer interface. It needs to be generic to handle any type of value the Observable might emit. We'll use the type parameter T to represent the type of the value.

The interface will have three methods:

  • next(value: T): void: Called by the Observable to deliver a new value. The value's type is T.
  • error(err: any): void: Called by the Observable to signal an error. The stream is terminated, and no more values will be delivered. We use any for the error type for practical reasons, as errors thrown in JavaScript can be of any type.
  • complete(): void: Called by the Observable to signal that it has finished emitting values. The stream is terminated.

Notice that all methods return void. The Observer's role is to react to notifications, not to return values to the Observable. Communication is unidirectional.

Your Task: Implement the Interface

  1. In your project's src directory, create a new file named Observer.ts.
  2. Add the following code to define and export our generic Observer interface.
// src/Observer.ts

/**
 * An Observer is a consumer of values delivered by an Observable.
 * It is an object with three methods: `next`, `error`, and `complete`.
 */
export interface Observer<T> {
  /**
   * Called by the Observable to emit a new value.
   * @param value The value emitted.
   */
  next: (value: T) => void;

  /**
   * Called by the Observable to signal an error.
   * After `error` is called, the Observable is considered terminated
   * and will not call any other methods on the Observer.
   * @param err The error that occurred.
   */
  error: (err: any) => void;

  /**
   * Called by the Observable to signal that it has finished emitting values.
   * After `complete` is called, the Observable is considered terminated
   * and will not call any other methods on the Observer.
   */
  complete: () => void;
}

We've included TSDoc comments to document our interface. This will be invaluable for generating API documentation later in the course.

3. The Observer Contract: The Rules of Engagement

Defining the shape of the Observer is only half the story. The other half is understanding the "grammar" or protocol that governs its use. An Observable must adhere to the following contract when interacting with an Observer:

An Observable can make zero or more calls to observer.next(). After it calls either observer.error() or observer.complete(), it must not make any further calls to any of the Observer's methods.

This contract ensures that the stream has a well-defined lifecycle. To see this in action, let's return to the Academind video, which has a fantastic demonstration of creating a custom Observable and manually calling the Observer's methods.

OBSERVABLES, OBSERVERS & SUBSCRIPTIONS | RxJS TUTORIAL

This section of the video is crucial. It shows the 'other side' of the interaction: how the producer function inside an Observable gets a reference to an Observer and calls its methods directly.

About seven and a half minutes in, watch Observer method calls. Observe how obs.next(), obs.error(), and obs.complete() are called. Notice what happens when obs.next() is called after obs.error() or obs.complete()—this directly illustrates the contract we just discussed.

This demonstration makes it clear that the Observer interface we designed is the public API that a producer function uses to communicate with the outside world. When we implement our Observable class in a future lesson, its core logic will be responsible for obtaining an Observer and calling these methods at the appropriate times.

Conclusion

In this lesson, we established the conceptual and practical foundation for the consumer side of our reactive library.

Key Takeaways:

  • The Observer is a core pattern in reactive programming, providing a standardized way to consume values from a data stream.
  • It is defined by an interface with three methods: next(value) for handling new data, error(err) for handling failures, and complete() for handling the end of the stream.
  • We created a generic Observer<T> interface in TypeScript, making it a reusable and type-safe component of our library.
  • We learned the fundamental "Observable Grammar": a stream can emit multiple values but terminates permanently after an error or complete signal.

Preview of the Next Lesson:

We've defined the "what" (the Observer that consumes data), but we haven't yet defined the "how" (how the connection is managed and torn down). In the next lesson, we will implement the Subscription class. This class will act as the handle to an active observation, giving us the crucial ability to unsubscribe and prevent memory leaks by cleaning up resources.

Can't find a good explanation? Sign up and we'll make it for you

Sign up