Typescript: Narrowing types by `typeof` inside `switch (true)` does not work

Created on 3 Mar 2020  Â·  13Comments  Â·  Source: microsoft/TypeScript

TypeScript Version: 3.8.3 Nightly


Search Terms:

  • switch statement
  • switch (true)

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:

Awaiting More Feedback Suggestion

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.

All 13 comments

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

seanzer picture seanzer  Â·  3Comments

Zlatkovsky picture Zlatkovsky  Â·  3Comments

wmaurer picture wmaurer  Â·  3Comments

Antony-Jones picture Antony-Jones  Â·  3Comments

dlaberge picture dlaberge  Â·  3Comments