Exhaustiveness Checking with ‘never’ Type in TypeScript

Updated: January 7, 2024 By: Guest Contributor Post a comment

Overview

Exhaustiveness checking in TypeScript ensures that every possible case is handled. The ‘never’ type, a lesser-sung hero, offers a fine tool to make your code more robust, by ensuring that certain code paths are unattainable or to flag a logical fallacy.

Getting Started With ‘never’

The ‘never’ type is a TypeScript feature introduced to represent the return type for functions with unreachable endpoint, such as functions that always throw an exception or those with an infinite loop. Let’s start by looking at a basic example:

function throwError(errorMsg: string): never {
  throw new Error(errorMsg);
}

Here, the function throwError will never successfully return a value; thus its return type is ‘never’.

‘never’ in Union Types and Type Guards

In TypeScript, union types allow you to define a variable as one of several types. Type guards enable you to check the type of a variable. Combining these with the ‘never’ type can be powerful:

function handleAnimal(animal: Dog | Cat | Fish): void {
  if (typeof animal === 'Dog') {
    // Handle Dog
  } else if (typeof animal === 'Cat') {
    // Handle Cat
  } else {
    const _exhaustiveCheck: never = animal;
    throw new Error('Unknown animal type');
  }
}

If animal happen to be a Fish, neither type guard would catch it, leading to an error being thrown – thanks to the ‘never’ type enforcing the check.

Ensuring Case Exhaustiveness in Enums

Enums in TypeScript are a method to organize a collection of related values. You can utilize a function that leverages the ‘never’ type to ensure that all cases in an enum are handled:

enum Directions {
  North,
  East,
  South,
  West,
}

function getTravelTime(direction: Directions): number {
  switch (direction) {
    case Directions.North:
      return 10;
    case Directions.East:
      return 5;
    case Directions.South:
      return 7;
    default:
      const exhaustiveCheck: never = direction;
      return exhaustiveCheck; // This line should never be reached
  }
}

If a new direction is added to the enum but not handled in the function, TypeScript will throw an error due to the type mismatch, prompting the developer to handle this case.

Advanced Patterns with ‘never’

Consider the following pattern, where we refine the type inspection with a function that asserts its argument is of the type never:

function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${value}`);
}

function processEvent(event: AppEvent): void {
  switch (event.type) {
    case 'MESSAGE':
      console.log('Message event processed.');
      break;
    case 'LOGIN':
      //... process LOGIN event
      break;
    default:
      assertNever(event);
  }
}

This helps in situations where new events may be added later on, ensuring that whenever the AppEvent union type is extended, the compiler will prompt you to update everywhere this type is used to handle the new case.

Use Case Based Discussion

We can further delve into use cases, such as discriminated unions or advanced type manipulation with utility types, providing practical scenarios and code examples where the ‘never’ type promotes safety and reliability in TypeScript applications.

Integration with Existing Codebases

Migrating your existing TypeScript code to utilize exhaustive checks can drastically improve how you catch issues, particularly in large applications. An approach to do this incrementally, without a complete overhaul, involves converting piece by piece, assuring a smooth transition to a more foolproof codebase.

Tooling and Compiler Configuration

TypeScript also comes with configuration options that enhance its strictness levels. For example, setting strictNullChecks to true will prevent null or undefined from being assigned to a variable unless expressly allowed. This enhances the integrity of your exhaustiveness checks throughout your application.

Conclusion

In the wild web of programming, TypeScript’s ‘never’ type serves as an unexpected compass, guiding you away from the perils of unconsidered conditions. Implementing this feature thoughtfully in your TypeScript artillery will prepare you for better code adventures, ensuring that you’re covering all possible paths, leaving no stone unreasoned thereby sparing your future self from uncertain desperations of bug-quashing.