Ver en Español
Implementing map as an operator on Observables
Apr 14, 2023
Updated: Jun 25, 2026

Implementing map as an operator on Observables

In this post we are going to explore how to grow the number of operators we can implement and use inside an Observable. First, let's recall the purest definition of an Observable, with a function that generates Observables from events:

// Define the "Observable" class
class Observable {
  // Constructor of the "Observable" class that accepts a "subscribe" argument
  constructor(subscribe) {
    // Assigns the value of the "subscribe" argument to the object's "_subscribe" property
    this._subscribe = subscribe;
  }
  // "subscribe" method that accepts an "observer" object
  subscribe(observer) {
    // Calls the function stored in the "_subscribe" property and passes it the "observer" object
    return this._subscribe(observer);
  }
  // Defines a static "fromEvent" method that accepts two arguments: "domElement" and "eventName"
  static fromEvent(domElement, eventName) {
    // Returns a new instance of the "Observable" class
    return new Observable(function subscribe(observer) {
      // Defines a "handler" function that accepts an "ev" argument
      const handler = (ev) => {
        // Calls the "next" method of the "observer" object and passes it the "ev" argument
        observer.next(ev);
      };
      // Adds an event listener to the DOM element "domElement" that listens for the event
      // specified in "eventName" and calls "handler" when the event occurs
      domElement.addEventListener(eventName, handler);
      // Returns an object that contains an "unsubscribe" method
      return {
        // Defines the "unsubscribe" method
        unsubscribe() {
          // Removes the event listener from the DOM element associated with "eventName" and "handler"
          domElement.removeEventListener(eventName, handler);
        },
      };
    });
  }
}

With this we can create Observables that return the event information like so:

const button = document.getElementById("button");
const clicks = Observable.fromEvent(button, "click");
const clicksSubscription = clicks.subscribe({
  next(e) {
    console.log("Click in the button", e); // "e" contains all the event information
  }
});

clicksSubscription.unsubscribe(); // This is how you can unsubscribe

Now, let's imagine we want to do more operations on the data stream, like getting only some of the event data, or listening only to clicks made on the left side of the button. Let's implement the map function on Observables.

The map function is a pure function that iterates over each element of an array and "maps" each item of an array into another:

[1, 2, 3].map(element => element * 2); // Returns [2, 4, 6]

To implement map on Observables, we will do the following:

class Observable {
  // ...
  map(projection) {
    const self = this;
    return new Observable(function subscribe(observer) {
      let subscription;
      subscription = self.subscribe({
        next(v) {
          let value;
          try {
            value = projection(v);
            observer.next(value);
          }
          catch (e) {
            observer.error(e);
            // We guard the cleanup: during the first synchronous emission
            // "subscription" may not be assigned yet.
            if (subscription) subscription.unsubscribe();
          }
        },
        error(e) {
          observer.error(e);
        },
        complete() {
          observer.complete();
        }
      });
      return subscription;
    });
  }
  // ...
}

One important detail in this code: inside the catch we call subscription.unsubscribe(), but subscription is the very variable we are assigning with that self.subscribe({...}) call. If the projection throws during the first synchronous emission, subscription would not be assigned yet and the cleanup would fail with a second error. That is why we guard it with if (subscription). In the fromEvent example the clicks are asynchronous, so this case rarely happens, but it is a good habit to keep the cleanup failure proof.

And now we can use map to transform the data inside our Observable, getting only the mouse position at the moment the button was clicked:

const button = document.getElementById("button");
const clicks = Observable.fromEvent(button, "click");
const clicksSubscription = clicks
  .map((ev) => ev.offsetX)
  .subscribe({
    next(offSetX) {
      console.log(offSetX);
    },
  });

With these snippets you have the full definition: the Observable class with its constructor and subscribe, the static fromEvent method, and the map operator. I recommend copying them into an online editor like CodePen or CodeSandbox and experimenting with them to see it all in action.

Conclusions

  • We have implemented the map function on Observables, which lets us perform transformations on event data in a more flexible way.
  • map on Observables is similar to its use on arrays, letting us apply a projection function to each element.
  • By using map on Observables, we can perform more advanced and custom operations on the data stream, which improves the quality and efficiency of our code.

Exercises to practice

  1. Implement the filter function on Observables to allow filtering events based on a specific condition.
  2. Create an Observable that returns keyboard events and use map to transform the output into objects that contain only the key pressed and the moment the event occurred.
  3. Combine the use of map and filter on Observables to process mouse events and return only the relevant information from those events that meet certain criteria.

3-point summary

  1. We have extended our Observable implementation with the map operator, which lets us transform event data in a more flexible and efficient way.
  2. The map function on Observables is similar to its use on arrays, applying a projection function to each element of the sequence.
  3. With map we can perform advanced and custom transformations on the data stream, which lets us handle events more effectively in our applications.

I hope this post was useful to you and helped you understand how to implement the map operator on Observables. If you have any questions or comments, don't hesitate to leave them in the comments section. Good luck with your projects, and don't forget to keep learning and practicing.

If you liked this post, don't forget to share it with your friends and colleagues on social media using the links below. See you next time!

Sebastian Gomez

Sebastian Gomez

Creador de contenido principalmente acerca de tecnología.

Leave a Reply

0 Comments

Advertisements

Related Posts

Categorias