How Do I Check That A Switch Block Is Exhaustive In Typescript

When working with TypeScript, one of the powerful features it offers is pattern matching through the switch statement. This statement allows you to handle different cases based on the value of an expression. However, it’s crucial to ensure that your switch block is exhaustive, meaning it covers all possible cases. In this article, we will explore how to achieve this in TypeScript, why it’s essential, and some best practices for writing exhaustive switch blocks.

Why Is Exhaustiveness Important?

Exhaustiveness in TypeScript’s switch blocks is essential for several reasons:

1. Type Safety

TypeScript is known for its strong typing system, which helps catch bugs at compile-time rather than runtime. When a switch block is exhaustive, TypeScript can ensure that you’ve handled all possible cases for a given type. This guarantees that your code is less error-prone and more reliable.

2. Avoiding Unintended Behavior

When a switch block is not exhaustive, you risk encountering unintended behavior in your code. Unhandled cases might result in unexpected errors or silent failures, making it challenging to debug and maintain your application.

3. Better Code Quality

Writing exhaustive switch blocks promotes better code quality by making your code more readable and maintainable. It explicitly states all the cases you’ve considered, making it easier for other developers (including future you) to understand your code.

Using Discriminated Unions

To achieve exhaustiveness in TypeScript switch blocks, you can leverage a feature called discriminated unions. A discriminated union is a type that has a common, shared property in all its members, known as a discriminant. Let’s go through an example to see how this works:

// Define a discriminated union
type Shape = { kind: 'circle'; radius: number } | { kind: 'rectangle'; width: number; height: number };

function calculateArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      return shape.width * shape.height;
  }
}

In this example, the Shape type is a discriminated union with two possible shapes: circle and rectangle. The kind property serves as the discriminant. When we use switch on shape.kind, TypeScript knows that we are handling all possible cases because kind only has two allowed values.

If we forget to handle one of the cases, TypeScript will raise a compilation error, prompting us to address the issue.

Using Exhaustiveness Checking

Starting from TypeScript 2.6, the language introduced a new feature called exhaustiveness checking for switch statements. This feature further enhances the safety and readability of your code. TypeScript can now analyze your switch block and check if all possible cases are handled, even without using discriminated unions.

Here’s how you can use exhaustiveness checking:

// Define a type
type Fruit = 'apple' | 'banana' | 'cherry';

function getFruitColor(fruit: Fruit): string {
  switch (fruit) {
    case 'apple':
      return 'red';
    case 'banana':
      return 'yellow';
    // TypeScript will raise an error here
  }
}

In this example, we have a Fruit type with three possible values. When we use switch on fruit, TypeScript can analyze the cases and identify that we’ve missed handling the ‘cherry’ case. It will raise a compilation error, ensuring that our switch block is exhaustive.

Handling Default Cases

In both discriminated unions and exhaustiveness checking, it’s a good practice to include a default case in your switch block. The default case acts as a safety net, handling unexpected or undefined values gracefully. Here’s an example:

type Day = 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday';

function getWeekdayMessage(day: Day): string {
  switch (day) {
    case 'Monday':
      return 'It\'s the start of the week!';
    case 'Tuesday':
      return 'You\'re doing great!';
    // Handle other days
    default:
      return 'Enjoy your day!';
  }
}

The default case here ensures that if day is not one of the specified weekdays, the function still returns a meaningful message instead of throwing an error.

Using Exhaustiveness in Enums

You can also achieve exhaustiveness when working with TypeScript enums. Enums are a convenient way to define a set of named constants. To ensure that your switch block covers all enum values, you can use the never type, which indicates that a value should never occur. Here’s an example:

enum Color {
  Red,
  Green,
  Blue,
}

function getColorName(color: Color): string {
  switch (color) {
    case Color.Red:
      return 'Red';
    case Color.Green:
      return 'Green';
    case Color.Blue:
      return 'Blue';
    default:
      const exhaustiveCheck: never = color;
      return exhaustiveCheck;
  }
}

In this example, the exhaustiveCheck variable is of type never, indicating that it should never be reached. If the default case is ever triggered, TypeScript will raise an error, ensuring exhaustiveness.

Frequently Asked Questions

What is an exhaustive switch block in TypeScript?

An exhaustive switch block in TypeScript is one that covers all possible values of a discriminated union or enum type. It ensures that there are no missing cases and provides a handling mechanism for every potential value of the type.

How can I check if my switch block is exhaustive in TypeScript?

TypeScript provides a built-in mechanism to check the exhaustiveness of a switch block. You can enable this by using the --strictNullChecks compiler option. TypeScript will then issue a compile-time error if your switch block is not exhaustive.

What happens if my switch block is not exhaustive?

If your switch block is not exhaustive and you’ve enabled --strictNullChecks, TypeScript will produce a compilation error. This error serves as a reminder to handle all possible cases to ensure type safety.

Can I use a default case in a switch block to make it exhaustive?

Yes, using a default case is a common approach to make a switch block exhaustive. The default case will catch any values that don’t match any of the explicitly defined cases. However, if your type allows for more specific handling, it’s better to handle each case explicitly to provide better type safety.

Are there any third-party libraries or tools that help with exhaustiveness checking?

Yes, there are third-party libraries like ts-expect and never that provide additional exhaustiveness checking beyond TypeScript’s built-in capabilities. These libraries can offer more advanced checking and allow you to write more concise and expressive code when working with discriminated unions.

Remember that ensuring exhaustiveness in switch blocks is essential for type safety, and TypeScript’s built-in checks (with --strictNullChecks) are usually sufficient. However, third-party libraries can be helpful for more complex scenarios.

Ensuring that your switch blocks are exhaustive in TypeScript is crucial for type safety, avoiding unintended behavior, and maintaining code quality. You can achieve this by using discriminated unions, leveraging exhaustiveness checking, and including default cases when necessary.

By following these best practices, you can write more reliable and maintainable TypeScript code, reducing the likelihood of runtime errors and making your codebase more understandable for both yourself and your fellow developers.

You may also like to know about:

Leave a Reply

Your email address will not be published. Required fields are marked *