DEV Community

Niklas Gruhn
Niklas Gruhn

Posted on

Avoiding "catch-all" else branches with type trickery

Avoiding "catch-all" else branches with type trickery - YouTube

"catch-all" else-branches can lead to unintended behavior when extra cases are introduced. We can leverage the type checker to catch such issues before going...

favicon youtube.com

"Catch-all" else branches can lead to unintended behavior when extra cases are silently introduced. We can leverage the type checker to catch such issues before going to production.

For example, here we have a TShirt type and a function, that returns the length of a TShirt in centimeters:

type TShirt = {
  size: 'small' | 'medium' | 'large'
}

function lengthCM(tshirt : TShirt) : number {
  if (tshirt.size === 'small') {
    return 70
  } else if (tshirt.size === 'medium') {
    return 72
  } else {
    return 74
  }
} 
Enter fullscreen mode Exit fullscreen mode

The final else branch is such a "catch-all" else branch. It handles the remaining case where size is "large". If we later add the size "xlarge" and forget to update the function, then we return the same length for both "large" and "xlarge".

It's better to handle each case explicitly:

//                                   vvvvvv  type error here
function lengthCM(tshirt : TShirt) : number {
  if (tshirt.size === 'small') {
    return 70
  } else if (tshirt.size === 'medium') {
    return 72
  } else if (tshirt.size === 'large') {
    return 74
  }

  // implicit:
  // return undefined
} 
Enter fullscreen mode Exit fullscreen mode

However, without any else branch we get an implicit return undefined at the end of our function. This causes a type error because the effective return type is now number | undefined instead of number.

The implicit return undefined should actually be unreachable because we exhaustively checked all T-shirt sizes. So a simple fix would be to throw an error:

function lengthCM(tshirt : TShirt) : number {
  if (tshirt.size === 'small') {
    return 70
  } else if (tshirt.size === 'medium') {
    return 72
  } else if (tshirt.size === 'large') {
    return 74
  }

  throw new Error('this should be unreachable')
} 
Enter fullscreen mode Exit fullscreen mode

Remember, when we introduce the size "xlarge", the throw statement becomes reachable. And because this is a runtime error it might be overlooked until the code is already in production. If possible it's always better to raise a type error, because type errors are detected during development.

To do that we introduce this weird little helper function:

function unreachable(witness : never) : never {
  return witness
}
Enter fullscreen mode Exit fullscreen mode

It's actually impossible to call this function without shushing the type checker, because to call it, you have to provide an argument of type never. never is "the empty type". There is no value that has type never. Except in the middle of unreachable code:

function lengthCM(tshirt : TShirt) : number {
  if (tshirt.size === 'small') {
    return 70
  } else if (tshirt.size === 'medium') {
    return 72
  } else if (tshirt.size === 'large') {
    return 74
  }

  unreachable(tshirt.size)
} 
Enter fullscreen mode Exit fullscreen mode

Here tshirt.size has type never because there is no value left that it could have at this point. The fact that we have access to a variable of type never is evidence that we are in the middle of unreachable code. So calling unreachable type-checks. BUT if the code ever becomes reachable (when we introduce "xlarge") we get a type error that reminds us to fix lengthCM before deploying to production:

function lengthCM(tshirt : TShirt) : number {
  if (tshirt.size === 'small') {
    return 70
  } else if (tshirt.size === 'medium') {
    return 72
  } else if (tshirt.size === 'large') {
    return 74
  } else if (tshirt.size === 'xlarge') {
    return 76
  }

  unreachable(tshirt.size)
} 
Enter fullscreen mode Exit fullscreen mode

Technically it is possible to always call unreachable by maliciously type casting to never:

unreachable("totally a string" as never)
Enter fullscreen mode Exit fullscreen mode

So in practice (and also for readability) I suggest this final implementation:

function unreachable(witness : never) : never {
  throw new Error('this should be unreachable')
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)