Retain the type of mutiple Observable<T> using combineLatest() from rxjs under Angular?
Asked Answered
A

3

17

When my component loads, I need to consume two services. Both need to be finished before it makes sense to continue. The order of completion of those is random and shouldn't be composed serially. The setup follows the pattern below.

const observables = [
  this.donkeyService.getDonkey(),
  this.monkeyService.getMonkey()
];

combineLatest(observables)
  .subscribe(([donkeyResult, monkeyResult]) => {
    if (!!donkeyResult && !!monkeyResult) {
      ...
    }
  }

We've noticed that the results aren't typed as expected. It took a while before we realized it, but the type of donkeyResult isn't Donkey, despite the definition of the server below!

getDonkey() : Observable<Donkey> { ... }

Finally, I realized that when the elements are delivered to the receiving array, they lose their typeness, since the array itself is any[]. So we manage it using casting as follows.

const observables = [
  this.donkeyService.getDonkey(),
  this.monkeyService.getMonkey()
];

combineLatest(observables)
  .subscribe(([_1, _2]) => {
    const donkeyResult = _1 as Donkey;
    const monkeyResult = _2 as Monkey;

    if (!!donkeyResult && !!monkeyResult) {
      ...
    }
  }

I'd like to refactor the code so that the array will retain the types specified by the service methods' signatures and won't coalesce into a common denominator any.

Is it possible to make TypeScript and/or Rxjs to have an array where the first element would be a Donkey and the second Monkey? Could I use a different data structure than an array?

I tried (and failed, of course) a bunch of different approaches including horsing around by casting the types directly in the array like so.

...
.subscribe(([_1 as Donkey, _2 as Monkey]) => { ... }

Is there a neat way to retain typeness? We're open to changing the approach altogether as we're owning this part of software and got some extra time to massage the code.

Amigo answered 22/5, 2019 at 6:0 Comment(4)
The first thing that came to my mind is to map the array into another object like map([donkey, monkey] => { return { donkey: donkey, monkey: monkey } as {donkey: Donkey, monkey: Monkey}; }). You still have to do the mapping by yourself with this thoBrevet
Wondering if you tried either of the following... .subscribe(([(_1 as Donkey), (_2 as Monkey)]) => { ... } ... or ... .subscribe(([<Donkey>_1, <Monkey>_2]) => { ... }.Corset
That's a tuple type, .subscribe(([_1, _2]: [Donkey, Monkey]) => .... Combine latest should track through up to 6 typed observables, though, so I'd guess the issue is the type of observables.Psychologist
Adding to the above answers , combineLatest<[Donkey, Monkey]>>(observables) .subscribe(([_1, _2]) => { const donkeyResult = _1; const monkeyResult = _2; if (!!donkeyResult && !!monkeyResult) { ... } }Brimmer
R
20

First things first, you have to type your function and your HTTP call.

getMonkey(): Observable<Donkey> {
  return this.http.get<Donkey>(url);
}

Once done, you have to type your array of observables :

const observables: [Monkey, Donkey] = [
  this.getMonkey(),
  this.getDonkey(),
];

Finally, although not necessary, you can type your callback params :

forkJoin(observables).subscribe(([monkey, donkey]: [Monkey, Donkey]) => {...});
Rights answered 22/5, 2019 at 6:30 Comment(3)
This is correct. Also this could be an alternative : forkJoin<[Monkey, Donkey]>, if the observables is not typed.Brimmer
This works with forkJoin, which is generic over the tuple type of the observables it's joining, but not for combineLatest, which is generic over the array element type. Specifically forkJoin<Monkey, Donkey> vs. combineLatest<Monkey | Donkey>, or combineLatest<any>.Kail
Adding types in the subscribe or pipe operator that follows combineLatest will work, but in that case it will be necessary whereas in the forkJoin example it won't be, as you've said.Kail
C
3

This sounds like a use case for tuples. https://visualstudiomagazine.com/articles/2016/02/01/type-safe-structures.aspx?m=1

type animalTuple = [Donkey, Monkey];
.subscribe(([donkeyResult, monkeyResult]: animalTuple) => { ... }
Corset answered 22/5, 2019 at 6:47 Comment(0)
K
1

using the map() operator

For those using the map and having the same typing problem, I solved passing the types to map using the diamond operators.

combineLatest([monkey, donkey].pipe(
  map<[Monkey, Donkey], ResultType>().(([monkey, donkey]) => {
    // here the variables 'monkey' and 'donkey' are correctly typed
  })
);
Kellyekellyn answered 27/12, 2023 at 15:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.