Rxjs: reduce operator typing gets lost

Created on 3 Sep 2018  路  2Comments  路  Source: ReactiveX/rxjs

Bug Report

Current Behavior
When using reduce/scan, typings get lost on 6.3.1 version of rxjs

锘匡豢index.ts:5:27 - error TS2345: Argument of type 'MonoTypeOperatorFunction<any[]>' is not assignable to parameter of type 'OperatorFunction<string, any[]>'.
  Types of parameters 'source' and 'source' are incompatible.
    Type 'Observable<string>' is not assignable to type 'Observable<any[]>'.
      Type 'string' is not assignable to type 'any[]'.

5   return from(items).pipe(reduce((acc, item) => acc.concat([item]), []));

Reproduction

import { from, Observable } from "rxjs";
import { reduce } from "rxjs/operators";

function load(items: string[]): Observable<string[]> {
  return from(items).pipe(reduce((acc, item) => acc.concat([item]), []));
}

Expected behavior
It should not throw the typing error when trying to reduce.

Environment

  • Runtime: NodeJS 8.11
  • RxJS version: 6.3.1
  • Angular 6.2, but reproducible without as well.
TS types

Most helpful comment

tl;dr: I'm pretty sure this isn't a bug, and I think your code was getting around typechecking due to a bug in rxjs 6.2.1. I believe you need to be explicit about the types passed to reduce, and the following should type check and work correctly under rxjs 6.3.x+:

function load(items: string[]): Observable<string[]> {
  return from(items).pipe(reduce((acc, item: string) => acc.concat([item]), []));
}

(change is item: string instead of just item)


Full explanation (disclaimer - I don't work on rxjs, I just enjoy figuring this stuff out and I could be wrong):

I believe your code was getting around correct typing using the funky pipe<R>(...operations: OperatorFunction<any, any>[]): Observable<R>; pipe overload added in https://github.com/ReactiveX/rxjs/pull/3789 and removed in https://github.com/ReactiveX/rxjs/pull/3945 .

If you take your code under rxjs 6.2.2 and change it to:

function load(items: string[]): Observable<string[]> {
  const thingToReturn = from(items).pipe(reduce((acc, item) => acc.concat([item]), []));
  return thingToReturn;
}

You'll notice there's an error, because thingToReturn is an Observable<{}> (similarly removing the explicit Observable<string> return type annotation changes the fn return type to Observable<{}>).

I think what's happening in 6.2.2 is something along the lines of:

  • Your current use of reduce looks to TS like reduce<any[]>(args): MonoTypeOperatorFunction<any[]> (under any rxjs version). If you mouse over reduce in your code you get reduce<any[]>..., and mouse over the item arg to see that it is similarly any[], when it should actually be string.
  • TS is doing some inference based on the combination of your expression being in the return statement, combined with the Observable<string> return type and the errant pipe overload, to decide that you are calling .pipe<string[]>(op). The bad overload makes TS not care what the actual type params were for your reduce.
  • Taking the expression out of the return breaks that funky inference on pipe's type param, TS goes for {} instead because it has nothing better to go on, and that is not compatible with string[], hence the error.

In 6.3 the bad overload is gone, the inference doesn't happen even with the expression in the return, and TS errors because your reduce has type param any[], but pipe wants an operator with input of string.

In this case you need to tell TS explicitly that you are not using reduce<any[]> and are instead using reduce<string, string[]>. You can do this like reduce((acc, item: string) => acc.concat([item]), []) or reduce<string, string[]>((acc, item) => acc.concat([item]), []).

I'm not sure if there's anything that can be done in reduce.ts to allow TS to better infer when the two-type-param overload is wanted without the usage having to be specific. I don't think there is, but I could easily be missing something!

All 2 comments

tl;dr: I'm pretty sure this isn't a bug, and I think your code was getting around typechecking due to a bug in rxjs 6.2.1. I believe you need to be explicit about the types passed to reduce, and the following should type check and work correctly under rxjs 6.3.x+:

function load(items: string[]): Observable<string[]> {
  return from(items).pipe(reduce((acc, item: string) => acc.concat([item]), []));
}

(change is item: string instead of just item)


Full explanation (disclaimer - I don't work on rxjs, I just enjoy figuring this stuff out and I could be wrong):

I believe your code was getting around correct typing using the funky pipe<R>(...operations: OperatorFunction<any, any>[]): Observable<R>; pipe overload added in https://github.com/ReactiveX/rxjs/pull/3789 and removed in https://github.com/ReactiveX/rxjs/pull/3945 .

If you take your code under rxjs 6.2.2 and change it to:

function load(items: string[]): Observable<string[]> {
  const thingToReturn = from(items).pipe(reduce((acc, item) => acc.concat([item]), []));
  return thingToReturn;
}

You'll notice there's an error, because thingToReturn is an Observable<{}> (similarly removing the explicit Observable<string> return type annotation changes the fn return type to Observable<{}>).

I think what's happening in 6.2.2 is something along the lines of:

  • Your current use of reduce looks to TS like reduce<any[]>(args): MonoTypeOperatorFunction<any[]> (under any rxjs version). If you mouse over reduce in your code you get reduce<any[]>..., and mouse over the item arg to see that it is similarly any[], when it should actually be string.
  • TS is doing some inference based on the combination of your expression being in the return statement, combined with the Observable<string> return type and the errant pipe overload, to decide that you are calling .pipe<string[]>(op). The bad overload makes TS not care what the actual type params were for your reduce.
  • Taking the expression out of the return breaks that funky inference on pipe's type param, TS goes for {} instead because it has nothing better to go on, and that is not compatible with string[], hence the error.

In 6.3 the bad overload is gone, the inference doesn't happen even with the expression in the return, and TS errors because your reduce has type param any[], but pipe wants an operator with input of string.

In this case you need to tell TS explicitly that you are not using reduce<any[]> and are instead using reduce<string, string[]>. You can do this like reduce((acc, item: string) => acc.concat([item]), []) or reduce<string, string[]>((acc, item) => acc.concat([item]), []).

I'm not sure if there's anything that can be done in reduce.ts to allow TS to better infer when the two-type-param overload is wanted without the usage having to be specific. I don't think there is, but I could easily be missing something!

@jgbpercy
Great! This works for me. And your explanation is heuristic.

Was this page helpful?
0 / 5 - 0 ratings