Avoiding "catch-all" else branches with type trickery
Tags: typescript
Published: 2023-10-11 20:51:28

---
title: Avoiding "catch-all" else branches with type trickery
published: true
description: "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 to production.
tags: #typescript
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-10-11 19:35 +0000
---

{% embed https://youtube.com/shorts/GG81redpGwQ?feature=share %}

"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:

```ts
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
  }
} 
```
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:
```ts
//                                   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
} 
```
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:

```ts
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')
} 
```
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:
```ts
function unreachable(witness : never) : never {
  return witness
}
```
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:

```ts
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)
} 
```
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:
```ts
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)
} 
```
Technically it is possible to always call `unreachable` by maliciously type casting to `never`:
```ts
unreachable("totally a string" as never)
```
So in practice (and also for readability) I suggest this final implementation:
```ts
function unreachable(witness : never) : never {
  throw new Error('this should be unreachable')
}
```