Skip to content

Type 'true' constants behave differently than 'true' literal #29323

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

Closed
ooflorent opened this issue Jan 9, 2019 · 9 comments
Closed

Type 'true' constants behave differently than 'true' literal #29323

ooflorent opened this issue Jan 9, 2019 · 9 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed Domain: Control Flow The issue relates to control flow analysis

Comments

@ooflorent
Copy link

TypeScript Version: 3.3.0-dev.20190108

Search Terms:

  • type true
  • type propagation
  • type inference
  • constant condition
  • constant folding

Code

declare const __TRUE__: true;
function ensureValue<T>(value: T | null): T {
  if (__TRUE__) {
    if (value === null) {
      throw new Error();
    }
  }
  return value;
}

Expected behavior:

ensureValue has a correct return type when strictNullChecks is enabled.

In other words, if a constant is typed as true, I expect it to behave like true literal.

declare const __TRUE__: true
if (true) {
if (__TRUE__) {

In the above example, both if statements should infer the types the same way.

Actual behavior:

There is a type error when strictNullChecks is enabled:

$ tsc --strictNullChecks --noEmit test.ts
test.ts:8:3 - error TS2322: Type 'T | null' is not assignable to type 'T'.
  Type 'null' is not assignable to type 'T'.

8   return value;
    ~~~~~~~~~~~~~


Found 1 error.

Please note that if (true) works as expected.

Playground Link:

http://www.typescriptlang.org/play/#src=function%20ensureValue%3CT%3E(value%3A%20T%20%7C%20null)%3A%20T%20%7B%0D%0A%20%20if%20(true)%20%7B%0D%0A%20%20%20%20if%20(value%20%3D%3D%3D%20null)%20%7B%0D%0A%20%20%20%20%20%20throw%20new%20Error()%0D%0A%20%20%20%20%7D%0D%0A%20%20%7D%0D%0A%20%20return%20value%20%2F%2F%20OK%0D%0A%7D%0D%0A%0D%0Adeclare%20const%20__TRUE__%3A%20true%0D%0A%2F%2F%20Same%20error%20using%20variable%20declaration%3A%0D%0A%2F%2F%20const%20__TRUE__%3A%20true%20%3D%20true%0D%0Afunction%20ensureValueWithConstant%3CT%3E(value%3A%20T%20%7C%20null)%3A%20T%20%7B%0D%0A%20%20if%20(__TRUE__)%20%7B%0D%0A%20%20%20%20if%20(value%20%3D%3D%3D%20null)%20%7B%0D%0A%20%20%20%20%20%20throw%20new%20Error()%0D%0A%20%20%20%20%7D%0D%0A%20%20%7D%0D%0A%20%20return%20value%20%2F%2F%20Hmm%0D%0A%7D

Related Issues:

@weswigham weswigham added Experience Enhancement Noncontroversial enhancements Domain: Control Flow The issue relates to control flow analysis labels Jan 9, 2019
@Jessidhia
Copy link

Jessidhia commented Jan 10, 2019

I actually abuse this bug to have type-only assertions written as unreachable runtime code 😅

If I write code intended to assert types inside an if (false), typescript completely ignores the branch, not even typechecking it. If I do, however, write an if (!true), typescript will look inside the branch, and I get my type assertions ignored at runtime and stripped by the minifier.

image

I say this is related because the typeof of const foo = !true is false, but is not treated the same as the literal false, which is just the false version of this bug.

@TheLarkInn
Copy link
Member

// subscribe cc @DanielRosenwasser also :-)

@RyanCavanaugh RyanCavanaugh added Needs More Info The issue still hasn't been fully clarified and removed Experience Enhancement Noncontroversial enhancements labels Jan 15, 2019
@RyanCavanaugh
Copy link
Member

Background: The control flow graph creator can't resolve the types of expressions (doing so would create a circular dependency between passes of the compiler), but does have some logic for detecting if (true) {. Basically, TS thinks if (value === null) { might not always execute, because during the construction of the control flow graph, it is not yet known that __TRUE__ is true.

What's the reason for writing code like this? It seems like you'd only want to do this if you wanted __TRUE__ to sometimes not be true (in which case, the error is correct)?

@ooflorent
Copy link
Author

ooflorent commented Jan 16, 2019

What's the reason for writing code like this?

The goal is to be able to remove those checks using a minification pass at your own risk. I understand that the output code would break the type contract.

The above snippet could be written as follow:

function ensureValue<T>(value: T | null): T {
  if (__TRUE__) {
    if (value === null) {
      throw new Error();
    }
  }
  return value!;
  //          ^
}

But I tried to see if it was possible to achieve the same result without using TypeScript syntax extension.

Moreover, since __TRUE__ was typed as true and not boolean, it is strange that it doesn't behave like true literals.

@ljharb
Copy link
Contributor

ljharb commented Jan 16, 2019

It does seem odd that boolean isn't inherently composed of two smaller types, false and true, separate from this use case - I'd expect type narrowing to also apply to something like:

function f(x: boolean) {
  if (!x) { return; }
  // here `x` is known to be `true`
}

@jack-williams
Copy link
Collaborator

@ljharb boolean is just a union type.

type MapBool<T> = T extends true ? "true" : T extends false ? "false" : never;
type B = MapBool<boolean>; // "true" | "false";

Your example narrows correctly for me.

function f(x: boolean) {
  if (!x) {
    const f: false = x;
    return;
  }
  const t: true = x;
}

You need --strictNullChecks for the false case to work correctly.

@ljharb
Copy link
Contributor

ljharb commented Jan 16, 2019

In that case, why wouldn’t the OP’s type of true follow the variable around? I’m sure there’s something I’m missing.

@jack-williams
Copy link
Collaborator

jack-williams commented Jan 16, 2019

@ljharb

The control flow graph handles type narrowing, and is used to essentially answer the question:

"Is the test (value === null) reachable? Always/Sometimes/Never"

As @RyanCavanaugh already described, there are no types involved when building the graph. The reason the true type doesn't follow the variable around is because the compiler hasn't actually got to type checking yet.

From the point of view of the graph, and trying to answer the "Always/Sometimes/Never" reachability question, the identifier __TRUE__ might aswell read:

__I_AM_SOME_IDENTIFIER_WITH_ANY_ARBITRARY_VALUE__.

The graph doesn't know the identifier will always evaluate to true, so it marks (value === null) as sometimes reachable, rather than always. When the literal is used the graph can recognise the special syntactic form (without having to typecheck) and mark the conditional as always reachable.

(Please correct me if I'm wrong!)

@RyanCavanaugh
Copy link
Member

@jack-williams 💯%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed Domain: Control Flow The issue relates to control flow analysis
Projects
None yet
Development

No branches or pull requests

7 participants