Exhaustive Type Checking with TypeScript!
Tags: typescript, productivity, functional, tdd
Published: 2019-04-08 09:31:10

# Introduction

Wouldn't it be great if you could spot a bug the moment you wrote it down? That's what static type analysis is all about. Catch your bugs at *compile-time* as you type them. This is why TypeScript is such a major productivity booster; especially at the strictest setting. 

TypeScript is designed to be a superset of JavaScript, which is why certain features have to be unlocked through patterns and principals. One such great feature is Exhaustiveness Checking.

In short, Exhaustive Type Checking is when your static compiler checks that you're not leaving a possibility unchecked. It helps to think about how [null checking](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html) works. 

```ts
function test(x: string | null) {
    if (x === null) {
        return;
    }
    x; // type of x is string in remainder of function
}
```
The function `test` above accepts a parameter `x` which can be either a `string` or `null`. Notice that below the `if` block with the return statement, TypeScript knows that `x` can now only be `string`. 

If we now add another `if` block for the case when `x` is of type `string`:

```ts
function test(x: string | null) {
  if (x === null) {
    return
  }
  if (typeof x === 'string') {
    return;
  }
  x; // type of x is never the remainder of the function
}
```

Then `x` below that block is of type `never`. All possibilities for `x` have been exhausted. We're now ready to discuss exhaustiveness checks.

# Exhaustive Type Checking

TypeScript supports literal values as types. Suppose we have a function `makeDessert` that takes a fruit of type `Fruit` and returns a string value to represent the name of the dessert made.

```typescript
type Fruit = 'banana' | 'orange'
function makeDessert( fruit: Fruit ) {
  // make dessert from type Fruit
}
```

How do we get compiler to insure we've covered all possible values for `fruit` inside the `makeDessert` function? 

We start by checking all possibly values:

```typescript
type Fruit = 'banana' | 'orange'
function makeDessert( fruit: Fruit ) {
  switch( fruit ) {
    case 'banana': return 'Banana Shake'
    case 'orange': return 'Orange Juice'
  }
  x // x has type never here
}
```
Since we've checked for both `banana` and `orange`, `x` has a type `never` at the bottom of this switch statement. Now we want to *insure* this. 

From here, there are two ways to do this. The first is to specify the return type of the function

```typescript
function makeDessert( fruit: Fruit ): string {
  switch( fruit ) {
    case 'banana': return 'Banana Shake'
    case 'orange': return 'Orange Juice'
  }
}
```

Because we specify the return type, the compiler will let us know if we are not handling a specific case. However, currently, this benefit goes away if the want to `throw` below the switch statement. TypeScript will assume that `never` is an acceptable return situation. It may be important to handle situations where the income type is `any` and does not match our cases.

The second way--to use a function that only accept a parameter of type `never`, which is what `x` is at the bottom of the switch statement. That way, if the type `Fruit` changes, `x` will no longer be of type `never`. That would trigger a compiler error and just like that, we have exhaustiveness checking powered by the compiler.

Let's look at this:

```typescript
type Fruit = 'banana' | 'orange'

function exhaustiveCheck( param: never ) { }

function makeDessert( fruit: Fruit ) {
  switch( fruit ) {
    case 'banana': return 'Banana Shake'
    case 'orange': return 'Orange Juice'
  }
  exhaustiveCheck( fruit ) // ✅ no error
}
```

Now if `Fruit` included a value `mango`, we would receive a compiler error, informing us that `fruit` at the bottom of the switch statement can be `mango`.

```typescript
type Fruit = 'banana' | 'orange' | 'mango'

function exhaustiveCheck( param: never ) { }

function makeDessert( fruit: Fruit ) {
  switch( fruit ) {
    case 'banana': return 'Banana Shake'
    case 'orange': return 'Orange Juice'
  }
  exhaustiveCheck( fruit ) // 🚫 ERROR! `mango` is not assignable to type `never`
}
```

To fix this error, we have to handle `mango` as a possible value

```typescript
type Fruit = 'banana' | 'orange' | 'mango'

function exhaustiveCheck( param: never ) { }

function makeDessert( fruit: Fruit ) {
  switch( fruit ) {
    case 'banana': return 'Banana Shake'
    case 'orange': return 'Orange Juice'
    case 'mango' : return 'Mango Smoothie'
  }
  exhaustiveCheck( fruit ) // ✅ no error, all values handled.
}
```

Finally, just in case, we should protect ourselves against the possibility the value for fruit being `any`, which turns off type checking and allows any value to pass into makeDessert. We do this by throwing an error in the `exhaustiveCheck` function:

```typescript
function exhaustiveCheck( param: never ): never { 
  throw new Error( 'should not reach here' )
}
```
This solution is outlined in this StackOverflow issue:

https://stackoverflow.com/questions/39419170/how-do-i-check-that-a-switch-block-is-exhaustive-in-typescript

Can we achieve Exhaustiveness Checking in TypeScript in a less verbose way? A more functional way? 

Yes and yes! 😅

# Helper Library

I've put together a helper library

https://github.com/babakness/exhaustive-type-checking

Along with a full video tutorial 🎬

<a href="#"><img src="https://images-nsfopsxwtu.now.sh/exhaustive-with-play-overlay.png"></a>

To highlight an example, we can create `makeDessert` as follows:

```typescript
const makeFruit = matchConfig<Fruit>()
const makeDessert = match({
  banana: () => 'Banana Shake'
  orange: () => 'Orange Juice'
  mango: () => 'Mango Smoothie'
})
```

This pattern offers a more compose-able way to do exhaustive checking with the same auto-complete goodness the switch pattern enjoyed.

# Conclusion

Exhaustiveness checks can really help cut-down runtime bugs. Indeed, even with Test-Driven Development, if we never the write code to handle a situation, our code coverage  statistics wouldn't reflect that we didn't test for it either.

Enjoy and feel free to visit me on [Twitter](https://www.twitter.com/babakness) and on my new [YouTube Channel](https://www.youtube.com/channel/UC-tTyOjpcUioZMEQmBPek_g)