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...strictNullChecks: truestrictNullChecks: falsegetMaybeStringLenBoolNegation...strictNullChecks: truestrictNullChecks: falsegetMaybeStringLenBoolCoercionNegation...strictNullChecks: truestrictNullChecks: falsePlayground Link: http://www.typescriptlang.org/play/#code/PTAEBUHsHNoGwKagM4BcBOBLAxqgcgK5xwDCAFgtgNbKiQBm9oAhgHYAmKCSqFoqATwAOSBOnSR0LISOboAUCDqtQcTKyQB2ADSgA7mUyJQAJg6h6KjQDcxoMRPS02nAMzpOzOHuYDaDyWR5bEhWNFBoBFQAWV8AIwQAZQx1aAAZBBUAXlAACgBbeKSU1mgALhQS6FAAH1BrSEx2AEpQLIA+UABveVBQTCYCouSsUrasnNYiOFq6woEEkdTxnIIOBHp1BBbu3r7QdCiCdBU19g2t9gBuPYBfPcPUY5V5xaqAOkRS3hvbm+DQuFIjFhlUMqwAEKQSBwPAIaDMVCYUJtPKvYqjcqVTG1eqNHYdXZ9AZ5ACE6KWpVaPX2ByOJ1AZwuGmudwe9JeoMxn0y0B+8j+8gBYVQESisQWGNS4KhMJIkDE2GRrDhCKRKJyQ0llKxaBxdQaTVahJp-UGpNliDYWremOa1L2fUez0Z602LJufXuTo5oApHy+fLIvxuQA
Related Issues: #1806, #8322, #10564
Alleged bug aside,
It's very suspicious when you accept void as an input to a function.
void should basically only ever be used as a return type of a function.
It's also possible for a "void" function to actually return 999 or "hello, world", or some other value
I'm on mobile but,
http://www.typescriptlang.org/play/#code/PTAEBUHsHNoGwKagM4BcBOBLAxqgcgK5xwDCAFgtgNbKiQBm9oAhgHYAmKCSqFoqATwAOSBOnSR0LISOboAUCDqtQcTKyQB2ADSgA7mUyJQAJg6h6KjQDcxoMRPS02nAMzpOzOHuYDaDyWR5bEhWNFBoBFQAWV8AIwQAZQx1aAAZBBUAXlAACgBbeKSU1mgALhQS6FAAH1BrSEx2AEpQLIA+UABveVBQTCYCouSsUrasnNYiOFq6woEEkdTxnIIOBHp1BBbu3r7QdCiCdBU19g2t9gBuPYBfPcPUY5V5xaqAOkRS3hvbm+DQuFIjFhlUMqwAEKQSBwPAIaDMVCYUJtPKvYqjcqVTG1eqNHYdXZ9AZ5ACE6KWpVaPX2ByOJ1AZwuGmudwe9JeoMxn0y0B+8j+8gBYVQESisQWGNS4KhMJIkDE2GRrDhCKRKJyQ0llKxaBxdQaTVahJp-UGpNliDYWremOa1L2fUez0Z602LJufXuTo5oApHy+fLIvxuIRFoGwcVAFVyxs6hs4mrjoAAjEA
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.
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"
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 forvoidfor both compiler flags, while checking againstnulland undefined isvalidfor 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.
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.
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.
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.
Likely a duplicate of #32809; the first two functions are leaking false information about void.
Probably the right answer is that in all cases,
voidshouldn't narrow at all.
Oh good, @RyanCavanaugh agrees with me that the current typeguard behavior is wrong. :smiley:
Yep, seems like a duplicate of #32809.
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.
Most helpful comment
Likely a duplicate of #32809; the first two functions are leaking false information about
void.