Transformation Operators Reference

1. map and mapTo for Value Transformation

Operator Syntax Description Use Case
map map(val => transformation) Applies projection function to each value, transforms and emits result Data transformation, property extraction, calculations
mapTo mapTo(constantValue) Maps every emission to same constant value, ignoring source Convert events to signals, normalize values

Example: map and mapTo transformations

import { of, fromEvent } from 'rxjs';
import { map, mapTo } from 'rxjs/operators';

// map - transform values
of(1, 2, 3).pipe(
  map(x => x * 10)
).subscribe(val => console.log(val)); // 10, 20, 30

// map - extract object properties
of(
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 }
).pipe(
  map(user => user.name)
).subscribe(name => console.log(name)); // 'Alice', 'Bob'

// map - complex transformation
of(1, 2, 3, 4, 5).pipe(
  map(n => ({ value: n, squared: n * n, isEven: n % 2 === 0 }))
).subscribe(obj => console.log(obj));

// mapTo - convert all values to constant
fromEvent(document, 'click').pipe(
  mapTo('Clicked!')
).subscribe(msg => console.log(msg)); // Always 'Clicked!'

// mapTo - signal transformation
of('a', 'b', 'c').pipe(
  mapTo(1)
).subscribe(val => console.log(val)); // 1, 1, 1

2. pluck for Property Extraction DEPRECATED

Operator Syntax Description Modern Alternative
pluck pluck('prop1', 'prop2') Extracts nested property by path - deprecated in RxJS 8 map(obj => obj.prop1.prop2)

Example: pluck deprecated - use map instead

import { of } from 'rxjs';
import { map } from 'rxjs/operators';

const users = of(
  { name: 'Alice', address: { city: 'NYC', zip: '10001' } },
  { name: 'Bob', address: { city: 'LA', zip: '90001' } }
);

// OLD - pluck (deprecated)
// users.pipe(pluck('address', 'city'))

// NEW - use map instead (recommended)
users.pipe(
  map(user => user.address.city)
).subscribe(city => console.log(city)); // 'NYC', 'LA'

// Multiple property extraction with map
users.pipe(
  map(user => ({
    name: user.name,
    city: user.address.city
  }))
).subscribe(data => console.log(data));

// Safe property access with optional chaining
of(
  { name: 'Alice', address: { city: 'NYC' } },
  { name: 'Bob' } // Missing address
).pipe(
  map(user => user.address?.city ?? 'Unknown')
).subscribe(city => console.log(city)); // 'NYC', 'Unknown'
Warning: pluck is deprecated as of RxJS 8. Use map() with property access or optional chaining instead.

3. scan and reduce for Accumulation Operations

Operator Syntax Description Emission Pattern
scan scan((acc, val) => acc + val, seed) Accumulates values over time, emits intermediate results (like Array.reduce but emits each step) Emits on every source emission
reduce reduce((acc, val) => acc + val, seed) Accumulates all values, emits only final result when source completes Single emission on completion

Example: scan vs reduce accumulation

import { of } from 'rxjs';
import { scan, reduce } from 'rxjs/operators';

// scan - emits intermediate accumulations
of(1, 2, 3, 4, 5).pipe(
  scan((acc, val) => acc + val, 0)
).subscribe(total => console.log('Scan:', total));
// Output: 1, 3, 6, 10, 15 (running total)

// reduce - emits only final result
of(1, 2, 3, 4, 5).pipe(
  reduce((acc, val) => acc + val, 0)
).subscribe(total => console.log('Reduce:', total));
// Output: 15 (only final total)

// scan - track state over time
of(1, -2, 3, -4, 5).pipe(
  scan((acc, val) => ({
    sum: acc.sum + val,
    count: acc.count + 1,
    min: Math.min(acc.min, val),
    max: Math.max(acc.max, val)
  }), { sum: 0, count: 0, min: Infinity, max: -Infinity })
).subscribe(stats => console.log(stats));

// Practical: running balance
const transactions = of(100, -50, 200, -75, 50);
transactions.pipe(
  scan((balance, amount) => balance + amount, 0)
).subscribe(balance => console.log('Balance:', balance));
// 100, 50, 250, 175, 225

// Practical: collect items into array
of('a', 'b', 'c').pipe(
  scan((arr, item) => [...arr, item], [])
).subscribe(arr => console.log('Array:', arr));
// ['a'], ['a','b'], ['a','b','c']
Note: Use scan for real-time state tracking and reduce when you only need the final accumulated result.

4. buffer, bufferTime, and bufferCount Operators

Operator Syntax Description Buffer Trigger
buffer buffer(notifier$) Collects values until notifier emits, then emits buffered array Observable emission
bufferTime bufferTime(timespan) Buffers values for specified time period (ms), emits array Time interval
bufferCount bufferCount(size, startEvery?) Buffers specified number of values, emits array when count reached Value count
bufferToggle bufferToggle(open$, close) Starts buffering on open$ emission, stops on closing observable Start/stop observables
bufferWhen bufferWhen(() => closing$) Buffers values, closing observable factory determines when to emit Dynamic closing observable

Example: Buffering strategies

import { interval, fromEvent } from 'rxjs';
import { buffer, bufferTime, bufferCount, take } from 'rxjs/operators';

// buffer - collect until notifier emits
const clicks$ = fromEvent(document, 'click');
interval(1000).pipe(
  buffer(clicks$),
  take(3)
).subscribe(buffered => console.log('Buffered:', buffered));
// Emits array of interval values when user clicks

// bufferTime - time-based buffering
interval(100).pipe(
  bufferTime(1000),
  take(3)
).subscribe(arr => console.log('Buffer:', arr));
// Emits ~10 values every 1 second: [0,1,2...9], [10,11...19], etc.

// bufferCount - count-based buffering
interval(100).pipe(
  bufferCount(5),
  take(3)
).subscribe(arr => console.log('Count buffer:', arr));
// [0,1,2,3,4], [5,6,7,8,9], [10,11,12,13,14]

// bufferCount with overlap
interval(100).pipe(
  bufferCount(3, 1), // size 3, start every 1
  take(5)
).subscribe(arr => console.log('Sliding:', arr));
// [0,1,2], [1,2,3], [2,3,4], [3,4,5], [4,5,6]

// Practical: batch API calls
const userActions$ = fromEvent(button, 'click').pipe(
  map(() => ({ action: 'click', timestamp: Date.now() }))
);
userActions$.pipe(
  bufferTime(5000), // Collect 5 seconds of actions
  filter(actions => actions.length > 0)
).subscribe(batch => sendToAnalytics(batch));

5. concatMap, mergeMap, and switchMap Higher-order Operators

Operator Syntax Concurrency Behavior Use Case
concatMap concatMap(val => obs$) Sequential (1) Waits for inner observable to complete before processing next Order-critical operations, sequential API calls
mergeMap mergeMap(val => obs$, concurrent?) Unlimited (or limit) Subscribes to all inner observables concurrently, emits as they arrive Parallel operations, independent async tasks
switchMap switchMap(val => obs$) Latest only Cancels previous inner observable when new value arrives Search/typeahead, latest-only results, navigation

Example: Higher-order mapping strategies

import { of, interval } from 'rxjs';
import { concatMap, mergeMap, switchMap, take, delay } from 'rxjs/operators';

// concatMap - sequential execution (waits for completion)
of(1, 2, 3).pipe(
  concatMap(n => 
    of(n * 10).pipe(delay(1000)) // Each waits for previous
  )
).subscribe(val => console.log('Concat:', val));
// Output at: 1s: 10, 2s: 20, 3s: 30

// mergeMap - concurrent execution (all run in parallel)
of(1, 2, 3).pipe(
  mergeMap(n => 
    of(n * 10).pipe(delay(1000)) // All start immediately
  )
).subscribe(val => console.log('Merge:', val));
// Output at: 1s: 10, 20, 30 (all at once)

// switchMap - cancel previous (only latest completes)
interval(1000).pipe(
  take(3),
  switchMap(n => 
    interval(700).pipe(
      take(3),
      map(i => `${n}-${i}`)
    )
  )
).subscribe(val => console.log('Switch:', val));
// Cancels previous inner observables when new value arrives

// Practical: Search typeahead with switchMap
const searchInput$ = fromEvent(inputElement, 'input');
searchInput$.pipe(
  debounceTime(300),
  map(e => e.target.value),
  switchMap(query => 
    this.http.get(`/api/search?q=${query}`) // Cancels previous request
  )
).subscribe(results => displayResults(results));

// Practical: Sequential file uploads with concatMap
const files$ = from([file1, file2, file3]);
files$.pipe(
  concatMap(file => uploadFile(file)) // Upload one at a time
).subscribe(response => console.log('Uploaded:', response));

// Practical: Parallel API calls with mergeMap
const userIds$ = of(1, 2, 3, 4, 5);
userIds$.pipe(
  mergeMap(id => 
    this.http.get(`/api/users/${id}`), // All requests in parallel
    3 // Limit to 3 concurrent requests
  )
).subscribe(user => console.log(user));
Note: switchMap for latest-only (search), mergeMap for parallel (independent tasks), concatMap for sequential order (queue processing).

6. exhaustMap for Ignore-while-active Pattern

Operator Syntax Behavior Use Case
exhaustMap exhaustMap(val => obs$) Ignores new source values while inner observable is active, prevents overlapping Login/submit buttons, prevent double-click, rate limiting

Example: exhaustMap for preventing concurrent operations

import { fromEvent, of } from 'rxjs';
import { exhaustMap, delay, tap } from 'rxjs/operators';

// exhaustMap - ignore clicks while request is pending
const loginButton = document.getElementById('login-btn');
fromEvent(loginButton, 'click').pipe(
  exhaustMap(() => 
    this.http.post('/api/login', credentials).pipe(
      delay(2000) // Simulates API call
    )
  )
).subscribe(response => console.log('Login response:', response));
// Ignores additional clicks until first request completes

// Compare with other operators
const clicks$ = fromEvent(button, 'click');

// switchMap - cancels previous request
clicks$.pipe(
  switchMap(() => apiCall()) // Latest click cancels previous
);

// mergeMap - allows concurrent requests
clicks$.pipe(
  mergeMap(() => apiCall()) // Every click creates new request
);

// exhaustMap - ignores new clicks while busy
clicks$.pipe(
  exhaustMap(() => apiCall()) // Ignores clicks during request
);

// Practical: Form submission protection
const submitButton$ = fromEvent(submitBtn, 'click');
submitButton$.pipe(
  exhaustMap(() => 
    this.http.post('/api/submit', formData).pipe(
      tap(() => console.log('Submitting...')),
      catchError(err => {
        console.error('Submission failed');
        return EMPTY;
      })
    )
  )
).subscribe(response => {
  console.log('Success:', response);
  resetForm();
});

// Practical: Refresh button with cooldown
fromEvent(refreshBtn, 'click').pipe(
  exhaustMap(() => 
    concat(
      this.http.get('/api/data'),
      timer(5000) // 5s cooldown before accepting new clicks
    )
  )
).subscribe(data => updateUI(data));

// Practical: Game actions (prevent spam)
const attackButton$ = fromEvent(gameButton, 'click');
attackButton$.pipe(
  exhaustMap(() => 
    of('Attack!').pipe(
      tap(() => playAnimation()),
      delay(1000) // Animation duration
    )
  )
).subscribe(action => executeAction(action));

Section 3 Summary

  • map transforms each value, mapTo replaces with constant
  • pluck is deprecated - use map(obj => obj.property) instead
  • scan emits intermediate results, reduce emits only final result
  • buffer variants collect values into arrays based on time, count, or signals
  • switchMap (latest), mergeMap (concurrent), concatMap (sequential)
  • exhaustMap prevents overlapping operations - ideal for button clicks and submissions