Jeffrey Drake
Jeffrey Drake

Reputation: 815

Typescript Function Overload Incompatibility

I have been implementing an NgRx store and found myself using the pattern

 concatMap(action => of(action).pipe(
          withLatestFrom(this.store.pipe(select(fromBooks.getCollectionBookIds)))
        )),

(from bottom of https://ngrx.io/guide/effects)

Considering how much room it can take up, I wanted to write a handy utility operator to handle it.

So I came up with an implementation that should work:

export function concatMapLatestFrom<A extends Action>(
    ...xs: Array<ObservableInput<never>>
): OperatorFunction<A, unknown> {
    return function (source: Observable<A>): Observable<unknown> {
        return source.pipe(
            concatMap((action) => of(action).pipe(withLatestFrom(...xs))),
        );
    };
}

I wrote some properly typed overloads:

export function concatMapLatestFrom<X1, A extends Action>(
    source1: ObservableInput<X1>,
): { (source1: X1): OperatorFunction<A, [A, X1]> };

export function concatMapLatestFrom<X1, X2, A extends Action>(
    source1: ObservableInput<X1>,
    source2: ObservableInput<X2>,
): { (source1: X1, source2: X2): OperatorFunction<A, [A, X1, X2]> };

export function concatMapLatestFrom<X1, X2, X3, A extends Action>(
    source1: ObservableInput<X1>,
    source2: ObservableInput<X2>,
    source3: ObservableInput<X3>,
): {
    (source1: X1, source2: X2, source3: X3): OperatorFunction<
        A,
        [A, X1, X2, X3]
    >;
};

But for some reason it thinks the overload signature is not compatible with the implementation. If I comment out one of them, it repeats for the next one in line.

It escapes me what is actually wrong with this, and it would be nice for the compiler to tell me why it isn't compatible but unfortunately it doesn't give any details.

Upvotes: 1

Views: 182

Answers (2)

Andrei Gătej
Andrei Gătej

Reputation: 11934

I think the culprits are the never and unknown types you're using there.

So, if you have

of(1)
  .pipe(
    concatMapLatestFrom(of('john'), of(true))
  ).subscribe(observer)

then, if I understood correctly, the observer should receive a [number, string, boolean] tuple.

As far as I can tell, concatMapLatestFrom does not says that very clearly:

function concatMapLatestFrom (): OperatorFunction<A, unknown> {}

So, we've only got the first element of that tuple, which is the source's type. What would be left to do is to property infer the other types, based on the types of the ObservableInputs provided to concatMapLatestFrom.

with this in mind, here would be my approach:

export function concatMapLatestFrom<
  A extends Action,
  U extends Array<ObservableInput<any>>,
  R = Unshift<ObservedValueTupleFromArray<U>, A>
> (
  ...xs: U
): OperatorFunction<A, R> {
  return function (source: Observable<A>): Observable<R> {
    return source.pipe(
      concatMap((action) => of(action).pipe(withLatestFrom(...xs))),
    );
  };
}

The types used can be exported from 'rxjs':


// Mapped tuples: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-1.html#mapped-types-on-tuples-and-arrays
// E.g [Observable<number>, Observable<string>] --> [number, string]
export type ObservedValueTupleFromArray<X> =
  X extends Array<ObservableInput<any>>
  ? { [K in keyof X]: ObservedValueOf<X[K]> }
  : never;

// Simply extract the value
export type ObservedValueOf<O> = O extends ObservableInput<infer T> ? T : never;

// Unshift<[B, C], A> --> [A, B, C]
export type Unshift<X extends any[], Y> =
  ((arg: Y, ...rest: X) => any) extends ((...args: infer U) => any)
  ? U
  : never;

Source

Upvotes: 1

Donovan Hiland
Donovan Hiland

Reputation: 1489

When you're working with overloads the type information comes from the overloads and the implementation just needs to handle all the possible parameters. The type arguments and return type are going to change based on the selected overload so leaving that out will probably fix the issue.

export function concatMapLatestFrom(
  ...xs: Array<ObservableInput<never>>
) {
  return function(source: Observable<any>): Observable<unknown> {
    return source.pipe(
      concatMap(action => of(action).pipe(withLatestFrom(...xs)))
    );
  };
}

Upvotes: 0

Related Questions