angulartypescriptrxjsangular2-observablesrxjs-dom

Work around for more than 6 forkJoin parameters?


I have a need/desire to use more than six parameters in a forkJoin. Currently, based on the answer to another related question it doesn't appear possible to send more than 6 parameters to forkJoin.

However, based on the offical documentation, it says "forkJoin is an operator that takes any number of Observables which can be passed either as an array or directly as arguments."

forkJoin - Official Docs

Well, I'm doing that and I get an error TS2322:Type 'foo' is not assignable to type 'bar[]'.

In my research I've also found that it's best not to send the arguments as an array if you have promises that return different types, as that will type cast them to all the same type. - Source

Here's my code. I'm using the latest version of Typescript and Angular 4.

ngOnInit() {
    this.spinner.show();
    Observable.forkJoin(
        this.loadParams(),              // Returns an Observable<Object>
        this.service.getPromiseType1(), // The rest return Observable<Array>
        this.service.getPromiseType2(),
        this.service.getPromiseType3(),
        this.service.getPromiseType4(),
        this.service.getPromiseType5(),
        this.service.getPromiseType6(),
    ).finally(() => this.spinner.hide())
        .subscribe(
            ([param, promise1, promise2, promise3, promise4, promise5, promise6]) => {
                this.job = job;
                this.value1 = promise1;
                this.value2 = promise2;
                this.value3 = promise3;
                this.value4 = promise4;
                this.value5 = promise5;
                this.value6 = promise6;
            }, (error) => {errorHandlingFunction}
   });

If I remove any single parameter so that it's passing six parameters to forkJoin, it works fine. So my question is, in my case where I want to load the object observable and subsequent array observables all in one call, is there another way to do this? Is this a bug with forkJoin since the official documentation says it should be able to accept any number of Observables?

I've tried creating an Array of type Observable and using array.forEach() inside the forkJoin but it complains about returning type void. That seemed like a janky way of doing it anyhow.


Solution

  • As the answer explains in the question you linked, the maximum number of arguments is only constrained by the type definitions -- not the runtime source itself. The type definitions are useful because they declare the array element types that will be produced for the next step of the observable stream (the types for [param, promise1, promise2, ...] in your case).

    It sounds like strict type safety around the assignments in your subscription handler is what's causing the issue for you. Since you have more than 6 observables, it defaults the resulting parameters to a shared type which likely does not match the types of the fields that you are trying to assign.

    There are a few ways around this. You can cast the arguments in your subscription handler or you can add your own types yourself. Casting arguments is the quick-and-dirty solution, but it causes you to lose type safety. Adding types yourself would let you maintain type safety, but it could also mean that you end up with any number of factory method declarations. Place the below in a type definition file (*.d.ts) anywhere in your project. I like to place type definitions like this in a typings/ directory at a sibling level to my app/ directory.

    import { Observable, SubscribableOrPromise } from 'rxjs/Observable';
    
    declare module 'rxjs/observable/ForkJoinObservable' {
        namespace ForkJoinObservable {
            export function create<T, T2, T3, T4, T5, T6, T7>(v1: SubscribableOrPromise<T>, v2: SubscribableOrPromise<T2>, v3: SubscribableOrPromise<T3>, v4: SubscribableOrPromise<T4>, v5: SubscribableOrPromise<T5>, v6: SubscribableOrPromise<T6>, v7: SubscribableOrPromise<T7>): Observable<[T, T2, T3, T4, T5, T6, T7]>;
            export function create<T, T2, T3, T4, T5, T6, T7, R>(v1: SubscribableOrPromise<T>, v2: SubscribableOrPromise<T2>, v3: SubscribableOrPromise<T3>, v4: SubscribableOrPromise<T4>, v5: SubscribableOrPromise<T5>, v6: SubscribableOrPromise<T6>, v7: SubscribableOrPromise<T7>, project: (v1: T, v2: T2, v3: T3, v4: T4, v5: T5, v6: T6, v7: T7) => R): Observable<R>;
        }
    }
    

    This process is explained in more detail in the TypeScript documentation page for Declaration Merging.


    Edit: It looks like I'm using an older version of RxJS and the structure has changed a bit since. The following should be closer to the type declarations that should work with the current structure:

    declare module 'rxjs/Observable' {
        namespace Observable {
            export function forkJoin<T, T2, T3, T4, T5, T6, T7>(sources: [ObservableInput<T>, ObservableInput<T2>, ObservableInput<T3>, ObservableInput<T4>, ObservableInput<T5>, ObservableInput<T6>, ObservableInput<T7>]): Observable<[T, T2, T3, T4, T5, T6, T7]>;
            export function forkJoin<T, T2, T3, T4, T5, T6, T7>(v1: ObservableInput<T>, v2: ObservableInput<T2>, v3: ObservableInput<T3>, v4: ObservableInput<T4>, v5: ObservableInput<T5>, v6: ObservableInput<T6>, v7: ObservableInput<T7>): Observable<[T, T2, T3, T4, T5, T6, T7]>;
        }
    }
    

    I am basing these off of the current forkJoin type declarations.

    As far as the module augmentation, the code above modifies the type declarations for the module defined by the absolute path 'rxjs/Observable'. This is the same as the import path that you would use when importing the Observable class. The module as defined by RxJS exports the Observable class and its fields. Our augmentation to that module modifies it by using the namespace block. This allows us to add type declarations under the Observable namespace (e.g., type declarations for Observable.myFunctionOrField) which looks the same as calling static functions on it. Effectively, this declares the additional Observable.forkJoin function possibilities.