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-patternGetting started
The smallest useful thing you can do with it, and what each part means.
typescript
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 compilingtypescript
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');Advanced usage
Where the library earns its place over a simpler alternative.
typescript
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();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.
