0

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()"?)
halfer
  • 19,824
  • 17
  • 99
  • 186
Crowdpleasr
  • 3,574
  • 4
  • 21
  • 37

1 Answers1

2
  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.
ggradnig
  • 13,119
  • 2
  • 37
  • 61
  • That's very helpful, but still leaves a few questions as follows: – Crowdpleasr Oct 19 '19 at 18:41
  • 1. the official documentation states that map "Applies a given project function to each value emitted by the source Observable, and emits the resulting values as an Observable." You indicated `map` returns type R. Is R itself an Observable (i.e., Observable), or R is not an Observable, or not necessarily an Observable? – Crowdpleasr Oct 19 '19 at 18:41
  • i.e., what is type `R`? I did some websearches on `Typescript generic type R`, but didn't find any results. Does type `R` imply a certain object type/signature, or does it just mean "a possibly different type than type `T`"? – Crowdpleasr Oct 19 '19 at 19:17
  • Last question just to make sure I understand: `map` will automatically "caste" its output as `Observable`, but for `switchMap` the coder has to convert the output to `Observable` or `ObservableLike` "manually" if output is not already `ObservableLike`? – Crowdpleasr Oct 20 '19 at 03:12
  • Before I accept your answer, can we please clarify with respect to which seems to claim that `of()` treats array-like structures "as a whole," and not "one-by-one" (i.e. as a `stream`) as per my examples and your answer. – Crowdpleasr Oct 20 '19 at 03:30
  • 1. Yes, R should just indicate a different type than T that is on the same observable-order as T. I guess `O` for output would be clearer. The important part is that if T is not an observable, then the other type is also not an observable. – ggradnig Oct 20 '19 at 09:28
  • 2. `map` treats the given value **as is**, meaning that the type of the return value is the type of the notification that the next subscriber (or operator) receives. For `switchMap` you need to 'manually' provide an `ObservableLike` - yes. – ggradnig Oct 20 '19 at 09:28
  • 3. Yes, `of` takes a value of a type `T`, while `from` takes a value of type `ObservableLike`. That's the difference between the two. – ggradnig Oct 20 '19 at 09:30
  • Thank you! Final question: if `of` takes a type T, and `from` takes `ObservableLike` then why is `of()` treating the strings as `ObservableLike` in my examples? – Crowdpleasr Oct 20 '19 at 18:28