TypeScript Version: 3.8.3 Nightly
Search Terms:
Code
let x: string | undefined;
if (typeof x !== 'undefined') {
x.trim(); // works as expected
}
switch (true) {
case typeof x !== 'undefined':
x.trim(); // thinks x is potentially undefined
}
Expected behavior:
Narrowing types should work inside the case.
Actual behavior:
Any assertion in case seems to be disregarded
Playground Link: https://www.typescriptlang.org/play/index.html?ts=Nightly#code/DYUwLgBAHgXBDOYBOBLAdgcwgHwgVzQBMQAzdEQgbgChqUSIAKMATwAcQB7BqCAQgC8AiAHICxMmgoiAlBADe1CNAB0yFAFtGMyhAD0eiAHdOSANbwIAQ0sgoHAMZgK1AL614RlGAcALJsh4IHKKyg42IBCsHNzQ-EKi4qTkhCIwSsqq6lo6+oZgvugWcSiWbJzOaGAoVsDALPhEyVKEbkA
Related Issues:
switch(true)
Never seen code like that before 😄. If you rewrite the if statement to be roughly equivalent to the switch statement, you get the same failure:
let x: string | undefined;
if (true === (typeof x !== 'undefined')) {
x.trim(); // fails
}
Which makes this a duplicate of... well, I couldn't find an existing ticket. But I'm reasonably sure narrowing === true and === false patterns was rejected before because it adds perf costs for an uncommonly used idiom.
Not arguing at all that the shape is a bit odd, but if there are many variants for what you're switching over then it is more readable than a bunch of if/else if.
The crux of this was filed at #8934 discussed a bit at #2214, but I think the overall guidance is to write something along the lines of
switch (typeof foo) {
case "number":
// ...
break;
case "string":
// ...
break;
default:
// not undefined
}
In your case it would be the following:
switch (typeof foo) {
case "undefined":
break;
default:
// not undefined
}
Thanks @DanielRosenwasser - maybe I shouldn't have simplified the example so much :)
Switching over the type is not equivalent in case you have multiple possible matches that are staggered based on the value, e.g.:
switch(true) {
case typeof process.env.X !== 'undefined':
// we favor env var X over Y if it is defined
break;
case typeof process.env.Y !== 'undefined':
// we fall back to Y if X is not defined
break;
}
This is not possible by switching over the type only, it really needs the actual value.
Shall I close this issue as something that will never be fixed/implemented due to performance issues for an outlier case?
What's the advantage of
switch(true) {
case typeof process.env.X !== 'undefined':
// we favor env var X over Y if it is defined
break;
case typeof process.env.Y !== 'undefined':
// we fall back to Y if X is not defined
break;
}
over
if (typeof process.env.X !== 'undefined') {
// we favor env var X over Y if it is defined
} else if (typeof process.env.Y !== 'undefined') {
// we fall back to Y if X is not defined
}
?
Two come to mind:
• readability for big amounts of cases
• being able to use fall-throughs for overlapping cases
Having said that I understand it is an edge case. I came across this pattern in a bigger codebase and wanted to make a change. That's when I noticed that the types are not narrowed in each case.
The third advantage is that in some cases it tastes as poor man's pattern matching.
Has there been any opdate or in progress work on this?
Fourth adwantage is when having to narrow an input which can be a primitive or a specific object type/class instance:
function processInput (input: string | RegExp | SomeType) {
switch(true) {
case typeof input === 'string':
// <code for string....>
break
case input instanceof RegExp:
// <code for regex....>
break
case isSomeType(input):
// <code for SomeType....>
break
}
// <some more code....>
}
I also ran into this. Typescript says geocode and zips are possibly undefined even though the case statement rules that out as a possibility.
export default function SearchArea() {
let geocode = useSelector((s: T.State) => s.geocode)
let zips = useSelector((s: T.State) => s.zips)
let crs = useSelector((s: T.State) => s.crs)
let area = 'NONE'
switch (true) {
case Boolean(crs):
area = 'CUSTOM'
break
case Boolean(geocode):
area = geocode.address // TS2532: Object is possibly 'undefined'.
break
case Boolean(zips):
area = zips.features // TS2532: Object is possibly 'undefined'.
.slice(0, 5)
.map((z) => z.properties.zip)
.join(', ')
area += zips.features.length > 5 ? ', ...' : '' // TS2532: Object is possibly 'undefined'.
}
return (
<>
<Typography variant="overline">Current search area</Typography>
<Typography variant="body1" gutterBottom>
{area}
</Typography>
</>
)
}
@wavded Your case won’t be fixed by this because of https://github.com/microsoft/TypeScript/issues/16655.
@wavded use !! (doublebang) instead. You'll still have the issue of this thread but TS2532: Object is possibly 'undefined'. should be gone
The doublebang is just 2 consecutive NOT operations, the first converts the value to truthy/falsy and inverts it, the second undoes the inversion. eg:
!!'' === false // => true
!!{} === true // => true
!!0 === true // => false
So your switch would look something like this:
switch (true) {
case !!crs:
area = 'CUSTOM'
break
case !!geocode:
area = geocode.address
break
case !!zips:
area = zips.features
.slice(0, 5)
.map((z) => z.properties.zip)
.join(', ')
area += zips.features.length > 5 ? ', ...' : ''
}
@KilianKilmister I get the same error unfortunately.
@wavded Oh, my apologies, with the link @ExE-Boss shared i was just thinking about type inference of the Boolean() constructor, and didn't realise that the error would be the same.
So yes, you are having the same problem as mentioned in this issue.
The easiest fix got you would probably be to tell typescript that these values are non-nullable. eg. with the [Non-null assertion operator].
let geocode = useSelector((s: T.State) => s.geocode)! // <- note the added `!`
let zips = useSelector((s: T.State) => s.zips)!
let crs = useSelector((s: T.State) => s.crs)!
Check out the docs, they explain it.
I'd still suggest generally using the !! instead of Boolean() though. It's considered the more favourable practice
Most helpful comment
Two come to mind:
• readability for big amounts of cases
• being able to use fall-throughs for overlapping cases
Having said that I understand it is an edge case. I came across this pattern in a bigger codebase and wanted to make a change. That's when I noticed that the types are not narrowed in each case.