skip to content
Andrei Calazans

Enhancing Safety in TypeScript: Exhaustive Checks for Switch Cases

/ 2 min read

This article reviews how you can enforce switch case exhaustiveness by either using a global Eslint rule or explicitly enforcing it via the usage of assertNever.

assertNever approach

While you can use ESLINT rule that is type aware like switch-exhaustiveness-check

You can also make this approach more explicit allowing better control to when you want to make the switch-case strict or not.

/**
 * The `assertNever` function is used for exhaustiveness checks in TypeScript.
 * It acts as a safeguard in switch statements where all possible cases of an enum are meant to be handled.
 * If a new case is added to the enum and not handled in the switch, calling this function will cause a TypeScript
 * compile-time error, alerting the developer that a case has been missed.
 *
 * Usage:
 * In a switch statement, use the `assertNever` function in the default case. Pass the variable being switched on
 * to the function. If all enum cases are handled, the function will never be called. If an unhandled case exists,
 * TypeScript will throw an error because the function expects a type of `never`, which indicates an unreachable code
 * segment under normal circumstances.
 *
 * Example:
 * switch (myEnumValue) {
 *   case MyEnum.FirstCase:
 *     // Handle first case
 *     break;
 *   case MyEnum.SecondCase:
 *     // Handle second case
 *     break;
 *   default:
 *     assertNever(myEnumValue); // TypeScript will error if a new MyEnum case is not handled.
 * }
 */
export function assertNever(x: never): never {
  throw new Error(`Unexpected object: ${x}`);
}

Examples

This forces the switch-case to handle all cases.

switch (transferType) {
    case 'withdrawal':
    case 'deposit':
      currency = {
        code: transfer?.amount?.currency,
        type: 'fiat' as CurrencyType,
      };
      break;
    default:
      assertNever(transferType);

Even when you are returning something, you can return assertNever to ensure there is always a valid return.


return useMemo(() => {
    switch (execution) {
      case 'allowTaker':
        return formatMessage(messages.executionTrayOptionAllowTaker);
      case 'postOnly':
        return formatMessage(messages.executionTrayOptionPostOnly);
      default:
        return assertNever(execution);
    }
  }, [execution, formatMessage]);

This also allows you to explicitly opt out by returning undefined instead. Particularly nice to keep the return type consistent and not imply undefined.

switch (tradeType) {
      case 'Buy':
      case 'Sell':
        return Analytics.track('convert_tapped_source_asset', params);
      case 'ConvertTo':
        return Analytics.track('convert_tapped_target_asset', params);
      default:
        return undefined;
    }

Use this paired with the default-case lint rule to enforce everyone to always handle a default case intentionally.