Search code examples
angulartypescriptrxjsrxjs5

How can I convince typescript that it's ok to call Observable operators on a Subject?


Example code, using Angular 4.1.3, rxjs 5.3.0, typescript 2.2.2:

import { Subject } from 'rxjs'

export class Example {
  public response$: Subject<boolean>;

  public confirm(prompt: string): Subject<boolean> {
    // ...set up a confirmation dialog...
    this.response$ = new Subject<boolean>();
    return this.response$.first();
  }
}

When I try to compile this code, typescript complains about the last line:

The 'this' context of type 'Subject<boolean>' is not assignable to method's 'this' of type 'Observable<boolean>'.
  Types of property 'lift' are incompatible.
    Type '<R>(operator: Operator<boolean, R>) => Observable<boolean>' is not assignable to type '<R>(operator: Operator<boolean, R>) => Observable<R>'.
  Type 'Observable<boolean>' is not assignable to type 'Observable<R>'.
    Type 'boolean' is not assignable to type 'R'.

My sense is that this is saying that first() is a method of Observable, not Subject, but my understanding is that a Subject is also an Observable, so this should not be a problem. And, in fact, if I ignore the error the code compiles and runs fine.

Things I have tried:

  1. Various ways of importing the first operator explicitly, e.g. import 'rxjs/add/operator/first'. This does not change the error.
  2. Casting the Subject to an Observable before calling first(), e.g. return (this.response$ as Observable<boolean>).first();. This results in a different but similar error, which also seems incorrect to me: Type 'Observable<boolean>' is not assignable to type 'Subject<boolean>'. Property 'observers' is missing in type 'Observable<boolean>'.

How can I convince typescript that it is valid to call first() on a Subject?

To clarify in response to the discussion of return types below: the method may also have had an incorrect return type (should be Observable rather than Subject) but that appears to be a separate issue; changing the return type does not resolve the error described above. A further detail confirming this is that when vscode highlights the error, it only highlights this.response$, not the entire return line, suggesting that the problem is between this.response$ and first(), not between first() and the function's signature:

error highlighted in vscode


Solution

  • TL;DR: Stricter type checks introduced in TypeScript 2.4 are not compatible with RxJS < 5.4.2 (due to a bug). Your IDE is probably using TypeScript >= 2.4 for type checks. Either upgrade RxJS (recommended), downgrade TypeScript, or use the programatic solution Subject.asObservable together with a return type Observable<boolean>.

    Explanation:

    After the discussion, I looked into the RxJS 5.3.0 source code and found that the signatures of lift differ between Subject and Observable. Specifically, we can see the following difference:

    Subject.ts:

    lift<R>(operator: Operator<T, R>): Observable<T>
    

    Observable.ts:

    lift<R>(operator: Operator<T, R>): Observable<R>
    

    The Generic Type of the returned Observable is T in Subject, but R in Observable.

    The first() operator requires an Observable for its this context, that can't be matched by Subject due to the mismatching signatures of lift. This is very strange to me, as Subject extends Observable and therefore should have matching signatures for all its properties.

    Now here is the solution: TypeScript introduced stricter type checks with version 2.4 and indeed RxJS needed to catch up with that change by changing the signature of the lift method. This change can be seen in the changelog at version 5.4.2.:

    Subject: lift signature is now appropriate for stricter TypeScript 2.4 checks

    Here is the corresponding issue. It seems to have been a bug.

    You're running RxJS 5.3, so that's why you get the error.

    There are two options. Either upgrade RxJS (recommended) or use Subject.asObservable to change the type from Subject to Observable.

    As mentioned in my comment, the return type of your function needs to change to Observable<boolean> as well.

    Applied on your snipped, this should look like this:

    public confirm(prompt: string): Observable<boolean> {
        // ...set up a confirmation dialog...
        this.response$ = new Subject<boolean>();
        return this.response$.asObservable().first();
      }
    

    I see your TypeScript version is 2.2, but it may very well be that your IDE uses another TypeScript version for type checks.

    Alternative

    As mentioned by @cartant, Subject.lift is implemented to return a Subject, but that can't be expressed with TypeScript's types (no return type overloading, not even sure if there is a language that supports that).

    So, another solution is to ignore the type checks by casting to <any>. That way you can also return Subject.