Changing return type of an Observable with map

The type definitions of map operator are as follows:

export declare function map<T, R>(
  project: (value: T, index: number
) => R, thisArg?: any): OperatorFunction<T, R>;

As you can see, you can set generic types in the map operator call. In your example, the T value is the Butterfly, and the R value is the Larva.

public getButterfly(): Observable<Butterfly[]> {
  return http.get<Larva[]>('url').pipe(
    map<Larva, Butterfly>(larva => new Butterfly(larva.dna))
  );
}

I think the accepted answer has masked a bigger problem, and perhaps a confusion in your understanding of what map actually does in rxjs.

In 'normal' Javascript - the map() function operates directly on an array and runs a function for every item in the array. eg. [1,2,3].map(x => x + 1). You immediately get back a full array of the same length as the original - with the transformed items.

In rxjs it is a completely different function that's part of rxjs and only has the same name as Array's map (go find the sourcecode if you dare!).

The actual function you pass to the rxjs map() still takes a single value and returns a single value (so it looks very much like the pure javascript map) but instead of operating on an array it operates on a single value in a stream.

Ok so you already know RXJS is about streams! And you have a stream of larvae right! (I don't know if this is terrifying or adorable!)

Well you actually don't. With http.get you do have a stream, but it only contains ONE value and that ONE value is the entire response from your http call whatever it may be.

By the look of it you're returning an array of Larva objects from the get call, but as far as RXJS is concerned this is a stream of ONLY ONE item.

This is your original code (annotated):

  public getButterfly(): Observable<Butterfly[]> {
   return http.get<Larva[]>('url').pipe(
       map( larva => {

           // We are now inside the RXJS 'map' function, NOT a Javascript map
           // here larva is an ARRAY (assuming your HTTP call actually returns an array)
           // So if you execute larva.dna you'll get 'undefined' 
           // (because dna is not a property on a javascript array!)
           // So you will return a butterfly OBJECT, but initialized with 'undefined' DNA. Scary!

           return new Butterfly(larva.dna);
       })
   );
}

So what I think you actually want is this:

public getButterflies() {
    return http.get<Larva[]>('url').pipe(
        map( larvae => larvae.map(larva => new Butterfly(larva.dna)))
    );
}

If that doesn't immediately make sense here's what's happening:

  • You get a array back from the http call (larvae - which you're telling typescript is typed as Larva[])
  • We then run the standard javascript map function on that array for EACH item and create a butterfly for each. You then return that new butterfly array as a replacement item in the stream.
  • Remember you have a stream. But it is a stream of one item. And each item is an array.
  • Note: The output type doesn't need to be specified and is automatically Butterfly[]

By sticking in <Larva, Butterfly> you're just telling the compiler that's what it is so you don't get compile time errors. You're never changing anything.

PS. Sometimes I like to specify output type, when it's simple - in order to show mistakes inside the 'pipe'. I'll do this if I have several code paths, each of which must return the same. By constraining the output type it reveals more errors.

Tip: Use tap(x => console.log('Message', x) in the pipe to write to the console what you have at each stage like this:

public getButterflies() {
    return http.get<Larva[]>('url').pipe(
        tap( x => console.log('Before map', x) ),
        map( larvae => larvae.map(larva => new Butterfly(larva.dna)),
        tap( x => console.log('After map', x) )
    ));
}

Why is it called tap? Because pipes have taps! And a (regular water) tap lets you see what's inside a (regular water) pipe :-)