The Role of Operators in RxJS
The RxJS library provides the Observable primitive and extends it with operators that streamline working with asynchronous event streams. Although Observable serves as the core building block, operators enable declarative composition and transformation of data streams.
Operators are combined through the pipe method available on Observable. The following basic example demonstrates how to transform and filter a stream of mouse clicks so that subscribers are only notified when a click occurs within a specific element:
const rect = document.querySelector('.rect').getClientRects();
function isEventInElement(rect, clientX, clientY) {
if (clientX < rect.left || clientX >= rect.right) return false;
if (clientY < rect.top || clientY >= rect.bottom) return false;
return true;
}
fromEvent(document, 'click').pipe(
map((event: MouseEvent) => {
return {x: event.clientX, y: event.clientY};
}),
filter(({x, y}) => {
return isEventInElement(rect[0], x, y);
})
).subscribe(() => console.log('inside'));
Operators fall into several distinct categories. Below are some of the most frequently used operators grouped by their respective categories:
- Creation
- Filtering
- Transformation
- Combination
- Error handling
- Multicasting
How Operators Work Internally
An operator is essentially a function that accepts a source observable, creates a new observable internally, and establishes a subscription that links the two. The newly created observable is then returned from the operator function and continues down the chain of operators. The function signature for an operator looks as follows:
(source: Observable<T>) => Observable<R>
Consider this minimal operator that links a source observable to a subscriber while forwarding values along the chain:
function operator(source) {
return new Observable(observer => {
source.subscribe((value) => {
observer.next(value);
});
});
}
Usage of this operator looks like this:
fromEvent(document, 'click').pipe(
operator
).subscribe((value: MouseEvent) => console.log(value.clientX));
This implementation is not exhaustive since it omits handling for complete and error notifications. Still, it clearly illustrates the fundamental concept behind operators: a new Observable is created and returned, and a subscription to the source is initiated.
Higher-Order Operators
In RxJS, every operator is implemented as a higher-order function that accepts parameters and produces an operator function. The map operator, for instance, takes a transformation function that processes each value passing through the operator.
Our custom operator from earlier can easily be refactored into a higher-order function that accepts a transformation function, mirroring the behavior of map:
function customOperator(transform) {
return (source) => {
return new Observable(observer => {
source.subscribe((value) => {
const transformed = transform(value);
observer.next(transformed);
});
});
};
}
It would then be invoked in this manner:
fromEvent(document, 'click').pipe(
customOperator((event) => event.clientX)
).subscribe((value: MouseEvent) => console.log(value));
For more detailed guidance on creating custom operators, refer to this article.

