Skip to content

Inconsistency with boolean negation as void type guards with --strictNullChecks #33180

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
timhwang21 opened this issue Sep 1, 2019 · 11 comments · Fixed by #33434
Closed

Inconsistency with boolean negation as void type guards with --strictNullChecks #33180

timhwang21 opened this issue Sep 1, 2019 · 11 comments · Fixed by #33434
Labels
Bug A bug in TypeScript
Milestone

Comments

@timhwang21
Copy link

TypeScript Version: [email protected]

Search Terms: void guard strictNullChecks boolean negation refinement

Code

// Toggle strictNullChecks off and see the type error appear
// on line 7, while 2nd fn never errors and 3rd always errors
const getMaybeStringLen = (maybeString: string | void) => {
  if (maybeString === null || maybeString === undefined) {
    return undefined;
  }
  return maybeString.length;
};

const getMaybeStringLenBoolNegation = (maybeString: string | void) => {
  if (!maybeString) {
    return undefined;
  }
  return maybeString.length;
};

const getMaybeStringLenBoolCoercionNegation = (maybeString: string | void) => {
  if (!Boolean(maybeString)) {
    return undefined;
  }
  return maybeString.length;
};

Expected behavior:

  • strictNullChecks: false behaves less strictly than strictNullChecks: true. Admittedly a loose criterion, but it seems like the stricter check fails with strictNullChecks: false and passes with strictNullChecks: true, while the type coercive check passes in both cases.

My confusion stems from the fact that ! is a valid type guard for void for both compiler flags, while checking against null and undefined is valid for strictNullChecks: true only.

As a bonus, while ! is always valid, !Boolean(x) is never valid.

Actual behavior:

  • getMaybeStringLen...
    • ...typechecks with strictNullChecks: true
    • ...does NOT typecheck with strictNullChecks: false
  • getMaybeStringLenBoolNegation...
    • ...typechecks with strictNullChecks: true
    • ...typechecks with strictNullChecks: false
  • getMaybeStringLenBoolCoercionNegation...
    • ...does NOT typecheck with strictNullChecks: true
    • ...does NOT typecheck with strictNullChecks: false

Playground Link: http://www.typescriptlang.org/play/#code/PTAEBUHsHNoGwKagM4BcBOBLAxqgcgK5xwDCAFgtgNbKiQBm9oAhgHYAmKCSqFoqATwAOSBOnSR0LISOboAUCDqtQcTKyQB2ADSgA7mUyJQAJg6h6KjQDcxoMRPS02nAMzpOzOHuYDaDyWR5bEhWNFBoBFQAWV8AIwQAZQx1aAAZBBUAXlAACgBbeKSU1mgALhQS6FAAH1BrSEx2AEpQLIA+UABveVBQTCYCouSsUrasnNYiOFq6woEEkdTxnIIOBHp1BBbu3r7QdCiCdBU19g2t9gBuPYBfPcPUY5V5xaqAOkRS3hvbm+DQuFIjFhlUMqwAEKQSBwPAIaDMVCYUJtPKvYqjcqVTG1eqNHYdXZ9AZ5ACE6KWpVaPX2ByOJ1AZwuGmudwe9JeoMxn0y0B+8j+8gBYVQESisQWGNS4KhMJIkDE2GRrDhCKRKJyQ0llKxaBxdQaTVahJp-UGpNliDYWremOa1L2fUez0Z602LJufXuTo5oApHy+fLIvxuQA

Related Issues: #1806, #8322, #10564

@timhwang21
Copy link
Author

timhwang21 commented Sep 1, 2019

Sure, that was just a contrived example, though. A more realistic situation would be when handling some promise that has a .catch().

// foo's type is Promise<string | void>
const foo = new Promise<string>(res => res('foo')).catch((err: any) =>
  console.log('I don't know what option types are')
);

You await a function that returns a value like this, and now you have to deal with a string | void.

I think this is irrelevant to the current question, though. The fact that the function in my example accepts void isn't relevant, this happens with any NotVoid | void.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Sep 1, 2019

I would personally expect all 3 examples to error, no matter what.

Because void doesn't actually mean "no value". It isn't an empty type. It isn't even a unit type.

It really means "I may or may not have a value but don't try to use me!"

So, you could have a variable of type string|void... But it has a number value!

The only thing I would expect is that, to narrow string|void to string, you have to use typeof myVar == "string"

@timhwang21
Copy link
Author

timhwang21 commented Sep 1, 2019

Yes, that would also be much more consistent to me.

The issue body is a bit long, so to summarize:

My confusion stems from the fact that ! is a valid type guard for void for both compiler flags, while checking against null and undefined is valid for strictNullChecks: true only.

As a bonus, while ! is always valid, !Boolean(x) is never valid.

I do not think !maybeString should be a valid guard in any case. I also think that maybeString === null || maybeString === void should either be valid for both compiler flags, or invalid for both compiler flags.

Edit: Accidentally hit 'comment and close,' please ignore.

@timhwang21 timhwang21 reopened this Sep 1, 2019
@fatcerberus
Copy link

fatcerberus commented Sep 1, 2019

Yeah, to clarify on what @AnyhowStep said from a more solid theoretical basis: void is essentially the dual of any. Where any is the type system promising you you can do anything you want with it and it won't get in the way (but things may go sour at runtime), void is the opposite: the type system is telling you it will put any value it wants there, so don't get in its way. So for example, you can't narrow string | void in general because the void could itself be a string:

function foo(): string { return "foo"; }
let bar: () => void = foo;
let result: void | string = bar();

Here, if you're only allowed to see the type of bar, you would think result should be a falsy undefined (and therefore distinguishable from string), but it's not! It's actually a truthy string! So if you're handed a "value" of type void, you can't do anything with it. Your only option is to discard it.

@timhwang21
Copy link
Author

Thanks, that does clarify things.

Would you recommend I edit the original issue to focus more on the fact that !foo works as a valid type guard for void, and that comparing against undefined and null do in some cases? Because while I definitely do appreciate being informed about the theoretical underpinnings of void, I'm currently more curious about 1. the current behavior of Typescript, and 2. (tangentially) how to reconcile this with the fact that many failable async operations return Promise<Something | void> given how .catch() is treated.

@fatcerberus
Copy link

The fact that !foo works as a typeguard for void I personally would consider a bug, but I'm not sure it's changeable at this stage due to backward compatibility concerns.

It's generally safe to assume that void === undefined so long as you're only dealing with first-order functions: a function declared as returning void will produce a compile-time error if you try to actually return something from it. However, the minute you start dealing with higher-order functions such as callbacks it's no longer a safe assumption because any other return type is assignable to => void (as illustrated in my example above).


Now the above having been said, Promise<void>, interestingly, doesn't suffer from the above issue:

(async () => {
    async function foo(): Promise<string> { return "foo"; }
    let bar: () => Promise<void> = foo;  // <-- type error here
    let result: void | string = await bar();
})();

I'm not yet convinced that's enough to guarantee Promise<void> actually resolves to something falsy at runtime, though.

@jack-williams
Copy link
Collaborator

Likely a duplicate of #32809; the first two functions are leaking false information about void.

@fatcerberus
Copy link

Probably the right answer is that in all cases, void shouldn't narrow at all.

Oh good, @RyanCavanaugh agrees with me that the current typeguard behavior is wrong. 😃

@sandersn
Copy link
Member

sandersn commented Sep 3, 2019

Yep, seems like a duplicate of #32809.

@sandersn sandersn added the Duplicate An existing issue was already created label Sep 3, 2019
@sandersn sandersn closed this as completed Sep 3, 2019
@jack-williams
Copy link
Collaborator

I'm re-opening this because my change in #33199 does not fix this issue.

@sandersn Please feel free to re-open/label the issue if you disagree with my changes.

@jack-williams jack-williams reopened this Sep 14, 2019
@jack-williams jack-williams added Bug A bug in TypeScript and removed Duplicate An existing issue was already created labels Sep 14, 2019
@sandersn sandersn added this to the Backlog milestone Sep 16, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants