0
votes

I've seen many other posts on this topic, and have read the official (see below) and semi-official documentation such as https://www.learnrxjs.io/operators/transformation/switchmap.html, but I still haven't been able to internalize the difference between "map" and "switchMap" and am hoping to clarify with the concrete examples below.

Note as per official RxJS documentation:

With my incomplete understanding in mind, I made a few very simple examples, see StackBlitz https://stackblitz.com/edit/rxjs-xpicph?devtoolsheight=60, but still don't fully understand why some of the examples are producing the output they do, in the way they do.

Firstly, some very simple string examples:

// String Example 1

const source = of('World').pipe(
  map(x => `Hello ${x}!`)
);

source.subscribe(x => console.log(`SOURCE (map): ${x}`));

// SOURCE (map): Hello World!

OK, fair enough, I think I mainly get this.

  1. "Of" emits 'World' as value (or is the emission still an Observable at this stage?) to "pipe"
  2. "pipe" provides 'World' as an value (or is it still an Observable?) to "map"
  3. "map" projects this (value? Observable?) to 'Hello World', waiting for all the characters to complete, and returns this (value? Observable?) to "pipe"
  4. "pipe" then returns an Observable.

Hence we get the output: "Hello World"

// String Example 2

const otherSource = of('World').pipe(
  switchMap(x => `Hello ${x}!`)
);

otherSource.subscribe(x => console.log(`SOURCE (switchMap): ${x}`));

// SOURCE (switchMap): H
// SOURCE (switchMap): e
// SOURCE (switchMap): l
// SOURCE (switchMap): l
// SOURCE (switchMap): o
// SOURCE (switchMap):
// SOURCE (switchMap): W
// SOURCE (switchMap): o
// SOURCE (switchMap): r
// SOURCE (switchMap): l
// SOURCE (switchMap): d
// SOURCE (switchMap): !

WHOA! EXCUSE ME? WHAT JUST HAPPENED?

  1. "Of" emits 'World' as value (or is the emission still an Observable at this stage?) to "pipe"
  2. "pipe" provides 'World' as an value (or is it still an Observable?) to "switchMap",
  3. "switchMap" projects this (value? Observable?) to 'Hello World', but unlike "map" doesn't wait for all the characters to complete before outputting to "pipe" a series of Observables (or values?), and is it one Observable per character? or is it one Observable that emits once per each character?
  4. "pipe" then returns an Observable on each character?

QUESTION: What exactly is going on under the hood here, step-by-step in the chains above?

Let's move on to another simple set of examples, and then hopefully try to tie everything together:

// OBJECT EXAMPLES

const foo = {
  "first": 1,
  "second": 2
}

// OBJECT EXAMPLE 1

Object.keys(foo).forEach(obj=>of(foo[obj]).pipe(
  map(x=>x*2)
).subscribe(x => console.log(`SOURCE (map): ${x}`)))

// SOURCE (map): 2
// SOURCE (map): 4

OK, fair enough. That's seems pretty straightforward

// OBJECT EXAMPLE 2

Object.keys(foo).forEach(obj=>of(foo[obj]).pipe(
  switchMap(x=>of(x*2))  // WHY DO WE NEED ANOTHER "of()" HERE? "switchMap(x=>x*2)" DOESN'T COMPILE
).subscribe(x=> console.log(`SOURCE (switchMap): ${x}`)))

// SOURCE (switchMap): 2
// SOURCE (switchMap): 4

Reasonably clear, but WHY do we need to supply "of(x*2) to "switchMap"? In STRING EXAMPLE 2, "switchMap" seemed to emit like crazy and automatically wrap its output as Observable (or did "pipe" wrap the output as Observable?), but whatever the case, "switchMap" and "pipe" didn't need any extra "of()" or any other help wrapping the output as an Observable, but in OBJECT EXAMPLE 2, we explicitly need to provide the second "of()" to make sure the the output from "switchMap" is an observable, otherwise the code won't compile. (But for "map", we don't need to provide the second "of()"). Again, step-by-step, why the difference?

So, to summarize, I'd be extremely grateful if anyone can explain:

  1. At which point(s) in the chain(s) in the examples above are we dealing with values (i.e., emissions from Observables) and at which points with Observables?
  2. Why does "switchMap" provide the apparent parsing behavior seen in STRING EXAMPLE 2?
  3. Why do we need to provide "of()" to "switchMap" in OBJECT EXAMPLE 2, but not in STRING EXAMPLE 2? (And, similarly, why doesn't "map" need a second "of()"?)

Thank you in advance!!

1

1 Answers

1
votes
  1. For your example operators, these are the type conversions:

    • of: receives a parameter of type T, produces a single notification of type T, then completes
    • map: receives a parameter of type T => R, produces a notification of type R whenever it receives a notification of type T
    • switchMap receives a parameter of type T => ObservableLike<R>, produces a notification of type R whenever it receives a notification of type T

  1. I think that's the main confusion here. switchMap has a projection function that's expects ObservableLike<R> as a return type. The important part of this statement is ObservableLike.

It's sometimes counter-intuitive, but RxJS internally converts other types in Observables when possible, like:

  • Array or
  • Promise

When an Array is used in a place where ObservableLike can be provided, RxJS treats the array as a stream of values.

For example, say I converted this array into an observable:

from([1, 2, 3])

what I'd get on subscribe would be:

// 1
// 2
// 3

And as string is nothing more than Array<char>, RxJS tries to convert this string into a stream of it's characters. (char is not really a data type in JS, but string internally is an array)

i.e. these are equal:

  • from(['a', 'b', 'c'])
  • from('abc')

  1. I think that's explained with the above answer. You always need to provide an ObservableLike for switchMap, but the string type happens to be an ObservableLike - just a stream of characters.