2

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

6
  • I'm not sure what observable(false) does. Are you trying to assigne a boolean to an Observable ? If yes, you might use of(false). Commented Mar 2, 2022 at 8:30
  • 1
    The x is observable variable, that means that its "inner" value is boolean and initially set to false = observable(false). This concept is from Knockout. However the implementation is not important here, nor the interface name. This example should only illustrate the compilation problem. I left the interface name unchanged for better understanding for those who knows knockout.js. Commented Mar 2, 2022 at 8:36
  • We're seeing this too. Here are some more examples of errors to append to your code: type PaymentMethod = 'cash' | 'card'; const y: Observable<PaymentMethod> = observable('cash'); const z: Observable<number[]> = observable([]); Commented Mar 4, 2022 at 14:08
  • This doesn't seem to correspond to any change mentioned at devblogs.microsoft.com/typescript/announcing-typescript-4-6 -- and certainly not to either of the changes under "Breaking Changes" -- so I think it's a bug. So it's worth looking through github.com/microsoft/TypeScript/issues to see if it's already been reported, and -- if not -- report it yourself. Commented Mar 7, 2022 at 7:37
  • Interestingly, const w: Observable<any> = observable(false); works fine, and when I hover over the observable bit, I see function observable<boolean>(value: boolean): Observable<boolean>. So the Playground recognizes that observable(false) returns an Observable<boolean> when you write Observable<any>, but not when you actually write Observable<boolean>. Commented Mar 7, 2022 at 7:49

2 Answers 2

3
+50

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.)

Sign up to request clarification or add additional context in comments.

1 Comment

Although this is only partial solution and you have to replace every declaration observable(false) with observable<boolean>(false), it is better than casting every usage. So bounty deserved. Anyway, the complete solution without changing anything in the user code is preferred, all ideas are summarized in my own answer.
3

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.

2 Comments

I think the answer by @martin-s is preferred as it ensures observable returns the desired type. There's no need to explicitly set the type of the variable.
It is not as simple as that. Very often we use generic Subscribable as a field in classes. Then subclasses concretely implements it as observable, pureComputed or whatever needed.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.