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

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:

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 47
  • Comments: 20 (6 by maintainers)

Commits related to this issue

Most upvoted comments

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.

I have similar usecase:

switch (true) {
    case error instanceof CustomError1:
        return error.foo;
    case error instanceof CustomError2:
        return error.bar;
    case error instanceof Error:
        return String(error);
}

And I am using this syntax in javascript because it looks better than chain of if else. Sadly can not use in typescript

I try not to pollute open issues with fluff like “me too!”, but in the related closed thread I was surprised to see this commented as a ‘wontfix’ as it is not a ‘common pattern’. Those comments were in 2016. I find this pattern a very clean and elegant approach that can be reused across an entire codebase to consistently handle very different scenarios.

Anecdotally, I see this pattern more and more in the wild.

@sandersn @RyanCavanaugh Would you still consider this an uncommon pattern today?

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.

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....>
}

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.

Anecdotally, I see this pattern more and more in the wild.

Best argument I’ve heard for switch(true) is that sometimes you need to execute only one of lots of possible things and it most succinctly conveys that. While you can just else if a bunch of times, you have to read all the way through to make sure there’s no missing else etc.

It’s not often I need it but when I do then edge cases like this are a real shame.

Any updates on this issue?

In Kotlin, when expression is much better in terms of intuition and usability. https://kotlinlang.org/docs/control-flow.html#when-expression

when (x) {
    1 -> print("x == 1")
    2 -> print("x == 2")
    else -> {
        print("x is neither 1 nor 2")
    }
}

enum class Color {
    RED, GREEN, BLUE
}

when (getColor()) {
    Color.RED -> println("red")
    Color.GREEN -> println("green")
    Color.BLUE -> println("blue")
    // 'else' is not required because all cases are covered
}

when (getColor()) {
    Color.RED -> println("red") // no branches for GREEN and BLUE
    else -> println("not red") // 'else' is required
}

when {
    x.isOdd() -> print("x is odd")
    y.isEven() -> print("y is even")
    else -> print("x+y is odd")
}

Hoping TS can at least supports what OP mentioned.

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
}

?

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
}