From https://github.com/microsoft/vscode/issues/77921
TypeScript Version: 3.6.0-dev.20190810
Search Terms:
Code
function foo(target: Number | String) {
if (target instanceof String === true) {
target.toLowerCase();
}
}
Expected behavior:
No compile error.
Type of target in the conditional statement should be String
Actual behavior:
Error: Property 'toLowerCase' does not exist on type 'String | Number'.
Property 'toLowerCase' does not exist on type 'Number'.
The guarding works properly if you omit the explicit === true:
function foo(target: Number | String) {
if (target instanceof String) {
target.toLowerCase();
}
}
Playground Link:
Related Issues:
Narrowing forms have to be identified syntactically, so we pay a perf penalty for every possible form that's checked for. Since there's no real reason to write it this way, we don't consider === true forms
I feel like the code example in my original ticket was completely reasonable and the current behavior very counter-intuitive. I understand that changing the behavior is undesireable if it implies a performance penalty, but it would really help me out if someone could point out my mistake or what I should be doing differently to work around the issue.
Thanks in advance
@oliversalzburg You can write:
if( !(target instanceof HTMLElement) ) {
return;
}
Narrowing forms have to be identified syntactically, so we pay a perf penalty for every possible form that's checked for. Since there's no real reason to write it this way, we don't consider === true forms
if( !(target instanceof HTMLElement) ) {
return;
}
@ahejlsberg said the similar thing when he implemented this type guard.
What about cases like this one?
function foo(target: Number | String) {
const targetIsString = target instanceof String;
if (targetIsString) {
target.toLowerCase();
}
}
We are currently writing TS definitions for parts of our code base and it's not uncommon that a parameter to a JS function can be of several types, especially if some arguments are optional. This causes us to write code that checks the type of the argument and branches accordingly.
This often leads to TS reporting errors as discussed in this issue, even though the code is perfectly valid. This is confusing developers and forces them to write code to adapt to limitations of TS, which is unfortunate.
Regardless, thanks for the explanations. The cleanest solution appears to be to write multiple functions that are stricter in their expected argument types.
@oliversalzburg That's a different thing - TS doesn't track dependencies between variables like that, it can only narrow variables directly included in the conditional expression. There are many, many tickets about that exact pattern; but it's actually a very big can of worms to make it work in general ("What does the (narrowed) type of this variable imply for all the other ones in scope?"), so it's largely considered a design limitation that that doesn't work.
@RyanCavanaugh
Since there's no real reason to write it this way,
I think have a good reason for writing x === false instead of !x: I find the logical NOT operator hurts readabilityβI've lost count of how many times I've missed a ! when skimming through code. For this reason I would like to see narrowing work equally when using both forms.
Related: https://github.com/microsoft/TypeScript/issues/32956
For future reference, here's a better example:
declare const isNonNullable: <T>(t: T) => t is NonNullable<T>;
declare const t: string | undefined;
if (!isNonNullable(t)) {
} else {
t.toUpperCase(); // β
no error, good
}
if (isNonNullable(t) === false) {
} else {
t.toUpperCase(); // β unexpected error
}
if (isNonNullable(t)) {
t.toUpperCase(); // β
no error, good
}
if (isNonNullable(t) === true) {
t.toUpperCase(); // β unexpected error
}
Most helpful comment
For future reference, here's a better example: