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

Unexpected error: TS2365: Operator '!==' cannot be applied to types 'false' and 'true' #12794

Closed
DanTup opened this issue Dec 9, 2016 · 20 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@DanTup
Copy link

DanTup commented Dec 9, 2016

TypeScript Version: 2.1.4

This code in my VS Code extension has stopped compiling (without changes) recently.

if (context.globalState.get(stateKey, false) !== true)
    // blah

It returns the error:

src/user_config_prompts.ts(17,6):
    error TS2365: Operator '!==' cannot be applied to types 'false' and 'true'.

The full source is here and the build output here.

It seems like TypeScript has decided that the call to get can only return false, and therefore this comparison is bad; however I don't believe that's true - this method access stored state, so I can put some value in it and then reload my extension and it would receive the stored value.

This may be the same as #12772, but the response is confusing to me:

the function expression is not considered part of the flow, since it is not guaranteed to execute

This logic seems flawed - you can't assume something can never have a certain value because it might not be called; surely you have to assume it might have that value because you do not know if it will be called?

@akarzazi
Copy link

akarzazi commented Dec 9, 2016

Indeed, there seems to be a bug in 2.1.4. (maybe a side effect of the new type inference).

interface A {
    get<T>(key: string, defaultValue: T): T;
}
let a: A;
var x = a.get("key", false); // Ok, x is boolean
var y = a.get("key", false) == true; // Error TS2365	Operator '==' cannot be applied to types 'false' and 'true'

@RyanCavanaugh
Copy link
Member

@ahejlsberg thoughts on how to properly type this?

@mhegazy
Copy link
Contributor

mhegazy commented Dec 12, 2016

you can define A as:

interface A {
    get(key: string, defaultValue: boolean): boolean;
    get<T>(key: string, defaultValue: T): T;
}

@RyanCavanaugh
Copy link
Member

Probably worth providing overloads on all the primitives in that case

@DanTup
Copy link
Author

DanTup commented Dec 12, 2016

I'm slightly confused - do you mean that all generic methods in all codebase like this will need to have loads of overloads (one for each primitive type)?

Surely it's incorrect to assume that if a literal value is passed as a generic arg that the return type is that specific value rather than its type? I can't imagine this would ever be the expected thing?

@mhegazy
Copy link
Contributor

mhegazy commented Dec 12, 2016

Surely it's incorrect to assume that if a literal value is passed as a generic arg that the return type is that specific value rather than its type? I can't imagine this would ever be the expected thing?

I do not see why this is true. the error message in the OP is a valid one. a.get("key", false) is false and not any boolean and a.get("key", false) == true is guaranteed to fail all the time. so not sure why i see this is a bad thing.

@DanTup
Copy link
Author

DanTup commented Dec 13, 2016

a.get("key", false) is false and not any boolean

I don't understand why this is the case. When someone defines a generic method like T get<T>(default: T) surely they are saying that return type is the same type as default and not it is the same value as default. What would be the point of the method if it always just returned the default value?!

a.get("key", false) == true is guaranteed to fail all the time

The implementation of this method is in VS Code; I can persist a value and read it back. The default I pass is for if there is no stored value. If the stored value is true, then this method returns true. I'm confused why you think this would fail (what use would a method be if it only ever returns the exact value you passed it?).

@mhegazy
Copy link
Contributor

mhegazy commented Dec 14, 2016

is the same type as default

true is a type, and so is false. boolean is true | false, and true === false is a comparison between two distinct types. nothing about values here. this is the same for numeric literals as well. 1 === 2 is an error as well.

The implementation of this method is in VS Code; I can persist a value and read it back. The default I pass is for if there is no stored value. If the stored value is true, then this method returns true. I'm confused why you think this would fail (what use would a method be if it only ever returns the exact value you passed it?).

the method declaration says it takes a type T and returns the same type T. why would it take a true and return boolean? would it be OK if the method took number | string and returned number?

@mhegazy mhegazy added the Working as Intended The behavior described is the intended behavior; this is not a bug label Dec 14, 2016
@DanTup
Copy link
Author

DanTup commented Dec 14, 2016

true is a type, and so is false. boolean is true | false, and true === false is a comparison between two distinct types. nothing about values here. this is the same for numeric literals as well. 1 === 2 is an error as well.

I can't currently test; but if I pass a 1 as defaultValue to the function above would the return type be "the 1 type" or a number? If the "the 1 type" then I think that's also crazy, if a number, then this seems inconsistent.

I'm sure there are many advantages to treating true and false as their own types, but this behaviour feels really wonky to me. It seems most natural to consider true and false to be values of type bool and having to cast them to bools all over the place seems unnecessary and unwanted :(

@akarzazi
Copy link

@mhegazy
This works fine in TS 2.0.
Providing overloads for all primitives, makes the generic version useless.
There is surely something wrong with the new type inference.

This is a widely used pattern.

From : https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack/Configuration/AppSettingsBase.cs

 public virtual T Get<T>(string name, T defaultValue)

https://github.com/ninject/Ninject/blob/master/src/Ninject/NinjectSettings.cs

public T Get<T>(string key, T defaultValue)

https://github.com/Microsoft/vscode/blob/5bf21a60b655546857f8162fa9b14a60d0e52280/src/vs/workbench/api/node/extHostExtensionService.ts

get<T>(key: string, defaultValue: T): T;

@mhegazy
Copy link
Contributor

mhegazy commented Dec 14, 2016

This works fine in TS 2.0.

This was an intentional change in TS 2.1, please see https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#literal-types-are-inferred-by-default-for-const-variables-and-readonly-properties. not treating literal types as types causes other errors as well. i would recommend adding overloads for the primitive values to ensure you are returning the base type instead of the literal type as suggested above.

@kipz
Copy link

kipz commented Jan 4, 2017

Perhaps I'm missing something here too, but shouldn't this compile as it used to pre 2.1?

function  getLengthAsStr(strs: string[]): string {
  if(strs.length !== 111){
     return "111"
  }
  if(strs.length !== 222){
    return "222"
  }
  return "blah"
}

results in:

Operator '!==' cannot be applied to types '111' and '222'.

There are a few things we can do to make it compile, such as asserting the type:

function  getLengthAsStr2(strs: string[]): string {
  if(strs.length !== <number>111){
     return "111"
  }
  if(strs.length !== <number>222){
    return "222"
  }
  return "blah"
}

or similarly:

function  getLengthAsStr3(strs: string[]): string {
  if((<number>strs.length) !== 111){
     return "111"
  }
  if((<number>strs.length) !== 222){
    return "222"
  }
  return "blah"
}

or weirdly, replacing a return statement with something else:

function  getLengthAsStr4(strs: string[]): string {
  if(strs.length !== 111){
    console.log("222")
  }
  if(strs.length !== 222){
    console.log("222")
  }
  return "blah"
}

@mhegazy
Copy link
Contributor

mhegazy commented Jan 4, 2017

function  getLengthAsStr(strs: string[]): string {
  if(strs.length !== 111){
     return "111"
  }
  // `strs.length` here can not be any thing other than `111`
  // this check is guaranteed to be true
  if(strs.length !== 222){
    return "222"
  }
  return "blah"
}

@kipz
Copy link

kipz commented Jan 5, 2017

Oh yeah, I see that, and cool that the compiler now catches that! :)

In sanitising the internal code, I removed the mutation:

function  getLengthAsStr(strs: string[]): string {
  if(strs.length !== 222){
     return "not 222"
  }
  //strs.length == 222
  strs.push("morestring")
 //strs.length == 223
  if(strs.length === 223){
    return "223"
  }
  return "blah"
}

which also fails to compile.

@ghost
Copy link

ghost commented Jan 15, 2017

Similarly, here's a snippet of from a lexer implementation:

    if (this.tip == "\"") {
      // ...
      let isEscaped = false;
      // NB: `this._advance()` will cause `this.tip` to differ from above
      for (this._advance(); this.tip != ""; this._advance()) { // TS2365
        if (this.tip == "\\" && !isEscaped) {
          isEscaped = true;
          continue;
        }

        if (this.tip == "\"" && !isEscaped) break; // TS2365

        // ...
      }
      // ...
    }

At both marked locations, buggy inference in tsc leads to spurious errors
because it treats this.tip as bound to ", presumably as a result of the
check on the first line.

error TS2365: Operator '!=' cannot be applied to types '"\""' and '""'.
error TS2365: Operator '==' cannot be applied to types '"\""' and '"\\"'.

In reality, this.tip differs at those two locations due side effects induced
by the calls to this._advance.

@aluanhaddad
Copy link
Contributor

@ghost
You could write it like this:

while (this._advance() && this.tip != "") {

  if (this.tip == "\\" && !isEscaped) {
    isEscaped = true;
    continue;
  }

  if (this.tip == "\"" && !isEscaped) break; // TS2365
}

where _advance returns a boolean and looks like

_advance(): this is this & { tip: string } { ... }

@endel
Copy link

endel commented Jan 16, 2017

I also had this error, and after spending some time I figured out that this error is erroneously appearing when there's unreachable code. And sometimes even for reachable code, without an explanation.

Example:

class TrueFalse {
    one: boolean = true;
    two: boolean = false;

    public test () {
        if (this.one == false && this.two == false) {
            console.log("that's true.")
        }
        else if (this.one == false) {
            console.log("that's false.")
        }
        else if (this.one == false) { // unreacheable code, removing gets rid of the error
            console.log("that's false.")
        }

    }
}


let t = new TrueFalse();
t.test();
Error: index.ts|12 col 18 error| Operator '==' cannot be applied to types 'true' and 'false'.

@aluanhaddad
Copy link
Contributor

Your second else if is actually a bug that the compiler is catching for you. It's dead code. The branch will never be taken because the previous else's if checks for the same condition

@endel
Copy link

endel commented Jan 16, 2017

@aluanhaddad I know, but the error message provided by the compiler is misleading. I also want to stress that sometimes this is happening even when there's no unreachable code, but I couldn't reproduce/isolate the scenario yet.

@kipz
Copy link

kipz commented Jan 16, 2017

@endel indeed, such as this: #12794 (comment)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

7 participants