Operators advanced

Signal Pipeline Operators

Create RxJS-style operators for transforming signal values with composable pipelines.

Alex Muturi
July 24, 2025
16 min read
#signals #operators #functional-programming #composition #pipelines

Signal Pipeline Operators

Create composable operators for signals inspired by RxJS, enabling elegant data transformation pipelines.

Complete Solution

import { signal, computed, effect, Signal } from '@angular/core';

export type SignalOperator<T, R> = (source: Signal<T>) => Signal<R>;

// Map operator
export function signalMap<T, R>(transform: (value: T) => R): SignalOperator<T, R> {
  return (source) => computed(() => transform(source()));
}

// Filter operator
export function signalFilter<T>(predicate: (value: T) => boolean): SignalOperator<T, T | undefined> {
  return (source) => computed(() => {
    const value = source();
    return predicate(value) ? value : undefined;
  });
}

// Debounce operator
export function debounce<T>(delayMs: number): SignalOperator<T, T> {
  return (source) => {
    const debounced = signal(source());
    let timeoutId: any;

    effect(() => {
      const value = source();
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => debounced.set(value), delayMs);
    });

    return debounced.asReadonly();
  };
}

// Throttle operator
export function throttle<T>(delayMs: number): SignalOperator<T, T> {
  return (source) => {
    const throttled = signal(source());
    let lastTime = 0;

    effect(() => {
      const value = source();
      const now = Date.now();
      if (now - lastTime >= delayMs) {
        throttled.set(value);
        lastTime = now;
      }
    });

    return throttled.asReadonly();
  };
}

// Distinct operator
export function distinct<T>(compareFn?: (a: T, b: T) => boolean): SignalOperator<T, T> {
  return (source) => {
    const distinct = signal(source());
    const compare = compareFn || ((a, b) => a === b);

    effect(() => {
      const value = source();
      if (!compare(value, distinct())) {
        distinct.set(value);
      }
    });

    return distinct.asReadonly();
  };
}

// Take operator
export function take<T>(count: number): SignalOperator<T, T | undefined> {
  return (source) => {
    const taken = signal<T | undefined>(source());
    let emitCount = 0;

    effect(() => {
      if (emitCount < count) {
        taken.set(source());
        emitCount++;
      }
    });

    return taken.asReadonly();
  };
}

// Pipe function for composition
export function pipe<T>(source: Signal<any>, ...operators: SignalOperator<any, any>[]): Signal<T> {
  return operators.reduce((acc, operator) => operator(acc), source);
}

Usage Examples

import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-operator-demo',
  standalone: true,
  imports: [FormsModule],
  template: `
    <input [(ngModel)]="searchInput" placeholder="Search..." />
    <p>Debounced: {{ debouncedSearch() }}</p>
    <p>Filtered: {{ filteredSearch() }}</p>
  `
})
export class OperatorDemoComponent {
  searchInput = signal('');

  // Compose operators
  debouncedSearch = pipe(
    this.searchInput,
    debounce(500),
    signalMap((value: string) => value.trim()),
    signalFilter((value: string) => value.length > 2),
    distinct()
  );

  filteredSearch = pipe(
    this.searchInput,
    signalMap((value: string) => value.toUpperCase())
  );
}

Key Takeaways

  1. Composable operators for clean data transformations
  2. Type-safe pipelines with full TypeScript support
  3. Reusable logic across components
  4. Functional programming style

Next Steps

References

Share this article