Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RxJS type inference fails in 3.6.2 #33131

Closed
felixfbecker opened this issue Aug 29, 2019 · 10 comments · Fixed by #33252
Closed

RxJS type inference fails in 3.6.2 #33131

felixfbecker opened this issue Aug 29, 2019 · 10 comments · Fixed by #33252
Assignees
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue High Priority

Comments

@felixfbecker
Copy link
Contributor

TypeScript Version: 3.6.2

Search Terms: type inference 3.6

Code

function asObservable(input: string | ObservableInput<string>): Observable<string> {
    return typeof input === 'string' ? of(input) : from(input)
}

Expected behavior:
This function managed to compile correctly in 3.5 with RxJS 6.5.2

Actual behavior:
In 3.6, it fails with

Type 'Observable<unknown>' is not assignable to type 'Observable<string>'.
  Type 'unknown' is not assignable to type 'string'.ts(2322)

the type of from() does not get inferred anymore.
It is declared as

function from<O extends ObservableInput<any>>(input: O): Observable<ObservedValueOf<O>>

Specifically, the ObservedValueOf<T> type fails now with ObservableInput<T>:

type ObservedValueOf<O> = O extends ObservableInput<infer T> ? T : never;

type ObservableInput<T> = SubscribableOrPromise<T> | ArrayLike<T> | Iterable<T>;

which worked before.

Related Issues: Filed at ReactiveX/rxjs#4992

@j-oliveras
Copy link
Contributor

Related to #33125?

@benlesh
Copy link

benlesh commented Aug 29, 2019

Yeah, this one is really not good for RxJS and will break a lot of people. This comment is a solid illustration of why

@maxdeviant
Copy link

Not using RxJS, but I can confirm that a number of spots in our codebase where type inference was working before (in 3.4.1) broke after upgrading to 3.6.2.

maxdeviant added a commit to remarksoftware/cardinal that referenced this issue Aug 29, 2019
There are apparently some type-inference issues in TypeScript 3.6:

microsoft/TypeScript#33131
@weswigham
Copy link
Member

weswigham commented Aug 29, 2019

So, this is my minimal repro:

declare function of<T>(a: T): Observable<T>;
declare function from<O extends ObservableInput<any>>(input: O): Observable<ObservedValueOf<O>>;

type ObservedValueOf<O> = O extends ObservableInput<infer T> ? T : never;

interface Subscribable<T> {
    subscribe(next?: (value: T) => void, error?: (error: any) => void, complete?: () => void): void;
}
type ObservableInput<T> = Subscribable<T> | Subscribable<never>;


declare class Observable<T> implements Subscribable<T> {
    subscribe(next?: (value: T) => void, error?: (error: any) => void, complete?: () => void): void;
}

function asObservable(input: string | ObservableInput<string>): Observable<string> {
    return typeof input === 'string' ? of(input) : from(input)
}

The Subscribable<never> is ultimately what produces the unknown inference result, looks like. If ObservableInput only contains a Subscribable<T>, there's no problem. I'll look into why our inference is unknown now~

@weswigham
Copy link
Member

Haaaaaaaaaaaaaaaaaaaaaah the issue is with our new inference matching code. We try to infer from Subscribable<never> to Subscribable<T> | Subscribable<never> - we match Subscribable<never> up to itself, strip it, then say "welp, nothing left in the source, must be done", and do not produce an inference for T. Now, since there's no inference for T, we assign it unknown, which then is union'ed with the inference from the other input type options once that flows out of the conditional and effectively erases 'em.

@fatcerberus
Copy link

fatcerberus commented Aug 30, 2019

We try to infer from Subscribable to Subscribable | Subscribable - we match Subscribable up to itself, strip it, then say "welp, nothing left in the source, must be done"

This actually makes perfect sense to me. Since Subscribable<never> matches itself, T is effectively a phantom type in that context. Like if you have type Maybe<T> = [ 'nil' ] | [ 'just', T ], then in the case of nil you can use it for any Maybe<T> you want because T isn't used in the resulting structure.

I guess it's not too nice if it breaks real-world code though. 😉

@fatcerberus
Copy link

fatcerberus commented Aug 30, 2019

Since Subscribable<never> matches itself, T is effectively a phantom type in that context.

While the above is true, I did subsequently realize it probably makes the internal union order observable in unexpected ways, which isn't too nice from a theoretical basis either*. So 👍 to #33154 from me.

* Always remember to put down the hood when done working on the car lest people start hot-wiring stuff after you walk away. 🚗 😛

@benlesh
Copy link

benlesh commented Sep 1, 2019

Here's another one that is broken:

import { defer, of } from 'rxjs';

const a$ = defer(() => of(1)); // $ExpectType Observable<number>

In 3.6, this is now Observable<never>

@DanielRosenwasser
Copy link
Member

Can you all try out [email protected] on npm?

@simeyla
Copy link

simeyla commented Dec 27, 2019

I've seen some cases where VSCode has chosen to put something like :

import { combineLatestMap } from 'src/app/shared/util/rxjs';

instead of

import { combineLatestMap } from '../../../../shared/util/rxjs';

I've seen this cause similar problems with inference - that sometimes work and sometimes don't.

So check your imports aren't absolute. (And yes this is a custom operator)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment