Tom HANAX
Tom HANAX

Reputation: 432

Type 'Observable<false>' is not assignable to type 'Observable<boolean>'

We are using Knockout.js (v3.5.0) and its TypeScript definitions. They worked OK until TypeScript 4.6.2. However the problem seems to be "deeper" than in the definitions file. It seems that there was some change in TypeScript in handling a boolean type. So rather than tagging this question as Knockout.js problem, I created small example of code inspired by the Knockout d.ts that illustrates the problem:

interface Observable<T>
{
  (): T;
  (value: T): any;
}

function observable<T>(value: T): Observable<T>
{
  return undefined as any;  // the implementation is not important
}

const x: Observable<boolean> = observable(false);

This code has one compilation problem:

Type 'Observable<false>' is not assignable to type 'Observable<boolean>'.
  Types of parameters 'value' and 'value' are incompatible.
    Type 'boolean' is not assignable to type 'false'.

Of course casting the false as boolean works, but I do consider this as hack, not a solution (obviously we need cast on every occurrence of true/false). Any way to actually solve this?

Edit: based on comment it is obvious that some type checking was changed. More examples can be seen here. Playground Link.

Is there any info (with explanation) for this change?

Edit2: as suggested in comments, I filed a bug report at https://github.com/microsoft/TypeScript/issues/48150

Upvotes: 2

Views: 1381

Answers (2)

Tom HANAX
Tom HANAX

Reputation: 432

According to the user who has responded to the Github issue (https://github.com/microsoft/TypeScript/issues/48150) the Typescript 4.6 compilation error is expected:

I believe this is a correct error which was not handled properly in old versions. The generic parameter T is invariant as it is used in both a covariant position () => T and a contravariant position (value: T) => any.

which is indeed true. Since user helped solve the problem, for sake of completeness I will try to rephrase and summarize his comments here.

The first proposed solution solves the problem only partially:

function observable<T>(value: T extends infer U ? U: never): Observable<T>
{
  return undefined as any;  // the implementation is not important
}

const x: Observable<boolean> = observable(false); // no error

however that will produce unknown type without a type annotation.

const bad = observable(false);
//     ^ unknown, which should be Observable<boolean>

The correct solution seems to introduce another type parameter

declare function observable<T, U = any>(value?: T extends infer R ? R : U): 
    unknown extends T ? Observable<U> : Observable<T>

Playground Link

As the main issue was with the Knockout.js library, I filed the issue there (https://github.com/knockout/knockout/issues/2589), hopefully someone with enough Typescript knowledge will correct the problem.

Upvotes: 3

Martin S
Martin S

Reputation: 91

I would suggest to instead explicitly specifying the generic type like:

const x = observable<boolean>(false);

The reason for this is that we are assuming that false implies boolean. Usually it does, but not always. Imagine e.g. these types

const x = observable<false | undefined>(false);
const y = observable(false);

(For anybody trying to solve the typing problem in any other way, please make sure that the above lines are also valid.)

Upvotes: 3

Related Questions