15

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.

BinaryButterfly
  • 18,137
  • 13
  • 50
  • 91
Konrad Viltersten
  • 36,151
  • 76
  • 250
  • 438
  • 1
    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 tho – Bunyamin Coskuner May 22 '19 at 06:09
  • 1
    Wondering if you tried either of the following... `.subscribe(([(_1 as Donkey), (_2 as Monkey)]) => { ... }` ... or ... `.subscribe(([_1, _2]) => { ... }`. – Dale Harris May 22 '19 at 06:27
  • 2
    That's a [tuple](https://www.typescriptlang.org/docs/handbook/basic-types.html#tuple) type, `.subscribe(([_1, _2]: [Donkey, Monkey]) => ...`. Combine latest should track through [up to 6](https://github.com/ReactiveX/rxjs/issues/4410) typed observables, though, so I'd guess the issue is the type of `observables`. – jonrsharpe May 22 '19 at 06:34
  • Adding to the above answers , `combineLatest<[Donkey, Monkey]>>(observables) .subscribe(([_1, _2]) => { const donkeyResult = _1; const monkeyResult = _2; if (!!donkeyResult && !!monkeyResult) { ... } }` – KiraAG May 22 '19 at 06:39

2 Answers2

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]) => {...});
  • 1
    This is correct. Also this could be an alternative : `forkJoin<[Monkey, Donkey]>`, if the observables is not typed. – KiraAG May 22 '19 at 06:42
  • 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` vs. `combineLatest`, or `combineLatest`. – Ran Lottem Aug 15 '19 at 14:17
  • 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. – Ran Lottem Aug 15 '19 at 14:38
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) => { ... }
Dale Harris
  • 550
  • 4
  • 14