← Back to archive

Repo of the Day

lukemorales/exhaustive

Published: Jan 30, 2023

Open repository ↗

Exhaustiveness checking in TypeScript

Summary

exhaustive is a small TypeScript library that turns object-literal "switch" mappings into compile-time exhaustiveness checks, with each case handler receiving the narrowed type for its variant. It also offers a corrupt helper to audit existing switch statements and a default-fallback mode for when you do not want runtime errors.

What it is useful for

The library helps engineers handle enums and discriminated (tagged) unions without forgetting a case. Common situations include mapping a Role enum to permissions, computing the area of a Shape union where each member has a kind discriminant, and replacing verbose switch chains with a readable object literal. It is also useful for refactoring existing code: dropping corrupt(day) into a default: branch makes TypeScript complain about unhandled cases, and it throws a TypeError at runtime if a corrupt value ever reaches that point. A _: () => ... key can be used when you would rather return a fallback than throw.

How engineers can use it

The README documents a single install command:

npm install exhaustive

For a plain enum, pass the value and a case map; the return value is inferred from the handlers:

import { exhaustive } from "exhaustive";

enum Role { ADMIN = 'ADMIN', DEFAULT = 'DEFAULT', VIEWER = 'VIEWER' }

const getUserPermissions = (role: Role) =>
  exhaustive(role, {
    ADMIN: () => [/* ... */],
    DEFAULT: () => [/* ... */],
    VIEWER: () => [/* ... */],
  });

For a tagged union, use exhaustive.tag and tell it which property discriminates members:

const area = (s: Shape) =>
  exhaustive.tag(s, 'kind', {
    square: (shape) => shape.size ** 2,
    rectangle: (shape) => shape.width * shape.height,
    circle: (shape) => Math.PI * shape.radius ** 2,
  });

In each handler, the parameter is narrowed, so shape is typed as Square, Rectangle, or Circle respectively. The README notes a TypeScript limitation: an overload exhaustive(s, 'kind', { ... }) exists, but autocomplete for the case keys only kicks in once you pass an empty object as the third argument, so exhaustive.tag is recommended for editor support.

For existing switch statements, add corrupt(day) in the default: branch; the compiler then reports Argument of type 'string' is not assignable to parameter of type 'never' whenever a case is missing. The library is published under the MIT license. For anything beyond what is covered above, the README is the source of truth.