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