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.