Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

exhaustive

lukemorales2.3kMIT1.1.2TypeScript support: included

Exhaustiveness checking in TypeScript

ts-pattern, exhaustive, match, pattern matching, exhaustive check, unreachable, corrupt

readme

Factory emoji

Exhaustive

Latest build Latest published version Bundlephobia Tree shaking available Types included License Number of downloads GitHub Stars

Exhaustiveness checking in TypeScript

Tiny bundle footprint for typesafe exhaustiveness checks with helpful type inference
to ensure you haven’t forgotten any case.

📦 Install

exhaustive is available as a package on NPM. Install it with your favorite package manager:

npm install exhaustive

⚡ Quick start

import { exhaustive } from "exhaustive";

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

enum Permission {
  DELETE = 'DELETE',
  EDIT = 'EDIT',
  VIEW = 'VIEW',
}

const getUserPermissions = (role: Role) =>
  exhaustive(role, {
    ADMIN: () => [Permission.DELETE, Permission.EDIT, Permission.VIEW],
    DEFAULT: () => [Permission.EDIT, Permission.VIEW],
    VIEWER: () => [Permission.VIEW],
  });

📝 Features

Tagged Unions

When working with Tagged Unions (or Discriminated Unions), use exhaustive.tag to inform what property to discriminate between union members:

interface Square {
  kind: 'square';
  size: number;
}

interface Rectangle {
  kind: 'rectangle';
  width: number;
  height: number;
}

interface Circle {
  kind: 'circle';
  radius: number;
}

type Shape = Square | Rectangle | Circle;

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

An overload is also available in the core exhaustive function: by adding a third parameter to the function, Typescript will fallback to the Tagged Union overload.

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

PS: Note that TypeScript has a limitation inferring the Tagged Union overload via argument types because they are generic values. This means autocomplete for the Tagged Union keys will not exist until you declare an empty object as the third argument:

exhaustive(s, 'kind', {});
//                     ^ this will trigger the Tagged Union overload

This overload exists so you can use it at your own convenience, but if you prefer the better DX of inferred types from the start, calling exhaustive.tag is still preferrable.

Type Narrowing

For every case checked, exhaustive will narrow the type of input:

const getRoleLabel = (r: Role) =>
  exhaustive(r, {
    ADMIN: (role) => capitalize(role), // Admin
//            ^? role is ADMIN
    DEFAULT: (role) => capitalize(role), // Default
//              ^? role is DEFAULT
    VIEWER: (role) => capitalize(role), // Viewer
//             ^? role is VIEWER
  });

const area = (s: Shape) => {
  return exhaustive.tag(s, 'kind', {
    square: (shape) => shape.size ** 2,
//             ^? shape is Square
    rectangle: (shape) => shape.width * shape.height,
//                ^? shape is Rectangle
    circle: (shape) => Math.PI * shape.radius ** 2,
//             ^? shape is Circle
  });
};

Default Fallback

If any corrupt values make to the exhaustive checker, it will throw a TypeError at runtime. If you don't want exhaustive to throw, you can provide a default fallback:

enum Food {
  BANANA = 'BANANA',
  SALAD = 'SALAD',
}

const getFoodType = (food: Food) => {
  return exhaustive(food, {
    BANANA: () => 'Fruit',
    SALAD: () => 'Leaves',
    _: () => 'Unknown',
  });
};

Exhaustive Switch Statements

Sometimes it's easier to work with switch statements, especially if you have a lot of cases that are falling-through to a common handler.

To enforce exhaustiveness checking inside switch statements, use the corrupt helper as your default value, which will make TypeScript complain of unhandled cases, and throw at runtime if the default case is reached:

import { corrupt } from "exhaustive";

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

const getLabelForDayOfWeek = (day: Day) => {
  switch (day) {
    case 'Monday':
    case 'Tuesday':
    case 'Wednesday':
    case 'Thursday':
    case 'Friday':
      return 'Weekday';
    case 'Saturday':
    // case 'Sunday':
      return 'Weekend';
    default:
      corrupt(day);
//             ^? Argument of type 'string' is not assignable to parameter of type 'never'
  }
};

changelog

exhaustive

1.1.2

Patch Changes

  • aec6f75 Thanks @lukemorales! - Improve inference of return types with new generic value

    The return type of exhaustive and exhaustive.tag will now be inferred based on the return type of the callback function.

    type Union = "IDLE" | "LOADING" | "SUCCESS" | "ERROR";
    
    type HumanReadableUnion = "idle" | "loading" | "success" | "error";
    
    function unionToHumanReadableString(union: Union): HumanReadableUnion {
      return exhaustive(union, {
        IDLE: () => "idle",
        LOADING: () => "loading",
        SUCCESS: () => "success",
        ERROR: (value) => value.toLowerCase(), // type `string` is not assignable to type `HumanReadableUnion`
      });
    }

1.1.1

Patch Changes

  • d210075 Thanks @lukemorales! - Fix exhaustive compile checks

    With the added support of exhaustive boolean checks, TypeScript stopped complaining if a key was missing in the exhaustive object due to how the new types were declared. The types were adjusted to bring back the expected behavior of TypesScript complaining at compile-time if you forget to handle all the use-cases.

1.1.0

Minor Changes

  • #6 abc000e Thanks @lukemorales! - ## Add support for booleans

    Both exhaustive and exhaustive.tag can now be exhaustive checked against booleans:

    function handleStatus(isSelected: boolean) {
      return exhaustive(isSelected, {
        true: () => {
          // ...run handler for true case
        },
        false: () => {
          // ...run handler for false case
        },
      });
    }
    type ProfileStatus =
      | { checked: true; data: string }
      | { checked: false; error: string };
    
    function handleProfileStatus(status: ProfileStatus) {
      return exhaustive.tag(status, "checked", {
        true: (value) => saveProfile(value.data),
        //           ^? value is { checked: true; data: string }
        false: (value) => throwException(value.error),
        //           ^? value is { checked: false; error: string }
      });
    }

1.0.0

Major Changes

  • #4 b287bbb Thanks @lukemorales! - ## Exposed Exhaustive types and added new generic slot in core function for type of output

    The types ExhaustiveUnion and Exhaustiveag are now exposed for your own convenience if you'd like to override the generic slots of exhaustive. The inferred output was also added as a new slot in the function generics, allowing you to also override the output value of exhaustive:

    import { exhaustive, type ExhaustiveTag } from "exhaustive";
    
    exhaustive<
      RequestState,
      "state",
      ExhaustiveTag<RequestState, "state">,
      JSX.Element
    >(request, "state", {
      IDLE: () => null,
      LOADING: (value) => <Loading />,
      SUCCESS: (value) => <List data={value.data} />,
      ERROR: (value) => <Error message={value.error} />,
    });

    BREAKING CHANGES

    Renamed _tag method to tag

    The _tag method was exposed as a some sort of secondary method to enhance the experience with Tagged Unions. Since the DX of using it is vastly superior compared to the Tagged Union overload in exhaustive, and it seems that we cannot improve the overload inference on the core funcion, the underscore has been removed from the method name and now you should used it as exhaustive.tag to make it official as the preferred way to exhaustive check Tagged Unions:

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

Minor Changes

  • #4 b287bbb Thanks @lukemorales! - ## Add exhaustive.tag overload to core function

    The same functionality of exhaustive.tag is available in the main function by adding a third argument to exhaustive that will trigger the Tagged Union overload.

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

    PS: Note that TypeScript has a limitation inferring the Tagged Union overload via argument types because they are generic values. Typescript will only fallback to the Tagged Union overload when you add a third argument. This means autocomplete for the Tagged Union keys will not exist until you declare an empty object as the third argument:

    exhaustive(shape, "kind", {});
    //                        ^ this will trigger the Tagged Union overload

    This feature is being added as a reflect of how the API was originally intended. Use it at your own convenience, but if you prefer the better DX of inferred types from the start, calling exhaustive.tag is still preferrable.

0.1.0

Minor Changes