Skip to content

ts-pattern

Developer UtilitiesUtilityTypeScript

What it is

ts-pattern brings exhaustive pattern matching to TypeScript, with full type narrowing and a compile-time guarantee that every case is handled.

match() takes a value and a chain of .with() branches. .exhaustive() makes the compiler reject the code if any possible case is unhandled.

Installation

npm install ts-pattern

Getting started

The smallest useful thing you can do with it, and what each part means.

Exhaustive matching on a union
import { match, P } from 'ts-pattern';

type State =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: string[] }
  | { status: 'error'; message: string };

const render = (state: State) =>
  match(state)
    .with({ status: 'idle' }, () => 'Nothing yet')
    .with({ status: 'loading' }, () => 'Loading…')
    .with({ status: 'success' }, ({ data }) => `${data.length} items`)
    .with({ status: 'error' }, ({ message }) => message)
    .exhaustive(); // add a fifth state and this line stops compiling
exhaustive() is the point of the library. A plain switch can be made exhaustive with a never check, but not across nested or multi-value patterns.
Nested and multi-value patterns
const describe = (response: Response) =>
  match(response)
    .with({ status: 200, body: { items: P.array() } }, ({ body }) =>
      `ok, ${body.items.length} items`)
    .with({ status: P.number.between(400, 499) }, ({ status }) =>
      `client error ${status}`)
    .with({ status: P.number.gte(500) }, () => 'server error')
    .otherwise(() => 'unexpected');

// Matching on a tuple of values at once.
const move = (from: Point, to: Point) =>
  match([from, to] as const)
    .with([{ x: 0, y: 0 }, P._], () => 'from origin')
    .otherwise(() => 'elsewhere');
Matching several values as a tuple replaces nested conditionals, and the branch callback receives correctly narrowed types throughout.

Advanced usage

Where the library earns its place over a simpler alternative.

Guards and selections
const result = match(user)
  .with(
    { role: 'admin', permissions: P.array(P.string) },
    (u) => u.permissions.length > 0,      // extra guard predicate
    (u) => `admin with ${u.permissions.length}`,
  )
  .with({ role: 'member', age: P.number.gte(18) }, () => 'adult member')
  .with({ role: 'member' }, () => 'minor member')
  .exhaustive();
The optional middle argument is a guard that runs after the structural match, letting you express conditions the pattern language cannot.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

Argument of type X is not assignable to NonExhaustiveError
This is the feature working — a case is unhandled. The error type names the shape you have not matched.
Branches do not narrow types
The value is not a discriminated union. Add a literal tag field so branches can be distinguished.

Best practices

  • Prefer .exhaustive() over .otherwise() — it turns a new union member into a compile error rather than a silent fallback.
  • Model application state as a discriminated union so matching stays meaningful.
  • Use P.select() to extract nested values instead of destructuring them by hand.
  • Do not replace simple two-branch conditionals; the value appears with three or more cases.

Background

Why it exists, and what it was reacting to.

Created by Gabriel Vergnaud, ts-pattern fills a gap left by TypeScript's switch statement, which cannot match on nested shapes and cannot prove exhaustiveness across complex unions.