# Strict & Weak Exhaustive Checks in TypeScript: Nuke your app at runtime for fun and profit!

This article assumes you are familiar with the never type, exhaustive checks, and the concept of failing fast. If you aren't yet, or want a refresher, here are some resources to get up to speed:

*  [When to use `never` and `unknown` in TypeScript](https://blog.logrocket.com/when-to-use-never-and-unknown-in-typescript-5e4d6c5799ad/)
* [The power of discriminated unions and exhaustiveness checking in Typescript](https://www.fullstory.com/blog/discriminated-unions-and-exhaustiveness-checking-in-typescript/)
* [Fail fast](https://www.martinfowler.com/ieeeSoftware/failFast.pdf)

Now I'm going to introduce you to two functions I wind up bringing into most TypeScript applications I work on, `isStrictNever` and `isWeakNever`

#### isStrictNever

```typescript
export const isStrictNever = (x: never): never => {
  throw new Error(`Never case reached with unexpected value ${x}`);
};
```

This function will throw if it is ever called at runtime, and our compile-time checks ensure that this function should never be called.


#### isWeakNever

```typescript
export const isWeakNever = (x: never): void => {
  console.error(`Never case reached with unexpected value ${x} in ${new Error().stack}`);
};
```

This function will only log at runtime, while still providing compile-time checks ensuring that this function should never be called.

### Using `isStrictNever` to fail fast

Out of the two functions, `isStrictNever` is preferred in most cases because it helps us fail fast. One place I always use this is if I'm performing any mapping or checks at the database layer. Data consistency at runtime is key, and can easily cause your application to fall into an unpredictable state if the system attempts to continue operating with bad data.

Here's an example:

```typescript
type Article = {
  author: string;
  body: string;
  title: string;
  type: ArticleType;
  publishedAt: Date;
};

type ArticleType = 'STANDARD' | 'SERIES';

export const selectLatestArticle = async (): Promise<Article | undefined> => {
  const res = await pool.query(`
    SELECT id, author, title, body, type_id, published_at 
    FROM articles
    ORDER BY published_at
    LIMIT 1;
  `);
  return res.rows.map(mapArticleRowToArticle).shift();
};

type ArticleRow = {
  author: string;
  body: string;
  title: string;
  type_id: ArticleTypeId;
  published_at: Date;
};

type ArticleTypeId = 1 | 2;

const mapArticleRowToArticle = ({published_at, type_id, ...row}: ArticleRow): Article => ({
  ...row,
  publishedAt: published_at
  type: mapArticleTypeIdToTypeString(type_id),
});

const mapArticleTypeIdToTypeString = (typeId: ArticleTypeId): ArticleType => {
  switch (typeId) {
    case 1: {
      return 'STANDARD';
    }
    case 2: {
      return 'SERIES';
    }
    default: {
      return isStrictNever(typeId);
    }
  }
};
```

Now, if I had a new article type id in the database, but forget to update the code to match, my application will crash whenever it retrieves an article using this new type.

You might think that sounds worse, but it's actually much better. This bug will likely cause end to end tests to fail completely. Because we failed fast, this change is unlikely to make it to production. Even if it does, we can easily track 50x coded HTTP responses in our observability system, we raise alerts when error rates spike, meaning our time to detection and time to recovery will be fast.

If instead we had simply let `mapArticleTypeIdToTypeString` return `undefined`, the impact might have been less obvious: it likely would have gone to production, and we would be dealing with disappointed product managers, a product that looks less professional, and have to prioritise bug tickets against new feature work. Given the option, I would much rather fail and fix up front. 

Not to mention the fact that latent bugs are vectors for new bugs to interact and cause additional unexpected behaviours, further undermining the reliability of our application.

But a simple `isStrictNever` can save us all of that pain. So why would we ever use `isWeakNever`?

### Using `isWeakNever` at the intersection of third party domains

Sometimes, we don't fully control the flow of data nor the implementation of types through our system. In these cases, we can use `isWeakNever` to create type systems that still help us enforce effective compile time checking for the parts of code we do control, while passing through unexpected runtime types from the code we do not control.

I often use this in a redux reducer, for example:

```typescript
type CountState = {
  count: number;
};

const initialState: CountState = {
  count: 0,
};

type IncrementAction = {
  type: 'INCREMENT';
}

type DecrementAction = {
  type: 'DECREMENT';
}

type CountAction = IncrementAction | DecrementAction;

export const reducer = (state: CountState = initialState, action: CountAction): CountState => {
  switch (action.type) {
    case 'INCREMENT': {
      return { count: state.count + 1 };
    }
    case 'DECREMENT': {
      return { count: state.count - 1 };
    }
    default: {
      isWeakNever(action);
      return state;
    }
  }
};
```

Now if I introduce a new count action:

```typescript
type SquareAction = {
  type: 'SQUARE';
};

type CountAction = IncrementAction | DecrementAction | SquareAction;
```

But neglect to add a new case to the reducer, I will receive a compile time error because `action` has a possible value in the default case.

This compile time check gives me certainty that my implementation has completeness, while allowing the app to continue running when Redux sends its `@@/INIT` action, a middleware dispatches an action, or even if we have multiple reducers combined together.

Yes, the type system is lying to us a little, but it is a known, controlled, and measured lie. Logging the uses of `isWeakNever` gives us visibility into the things that slip through. 

While Phryneas (AKA Lenz Weber) levies some valid criticisms towards the discriminated union approach to Redux reducers [in his article](https://phryneas.de/redux-typescript-no-discriminating-union), I believe this approach alleviates his concerns, while making the holes in the type system much more explicit.

This approach also enforces completeness through exhaustive checks, while his approach leaves these checks behind only to focus on payload type hinting.

In my opinion, exhaustive checks AND payload type hinting together are worthwhile, given an explicit indicator that the type system is incomplete, as usage of `isWeakNever` does.

Have another opinion on the matter? Share your thoughts in the comments below!
