ts-pattern brings pattern matching to TypeScript with exhaustive type checking — it ensures you handle every case of a union type at compile time. match(value).with(pattern, handler).exhaustive() throws a type error if a case is missing. P.union("a", "b") matches multiple values; P.instanceOf(Error) matches class instances; P.when(fn) applies custom guard functions. P.select("key") extracts nested values from a matched pattern. P.infer and P.narrow provide runtime narrowing for complex conditionals. ts-pattern replaces sprawling switch statements and if/else if chains with structured, verifiable matching. It integrates naturally with discriminated union result types, event handling, and state machine transitions. Claude Code generates ts-pattern match expressions, exhaustive union handlers, and the state machine dispatch patterns for TypeScript applications.
CLAUDE.md for ts-pattern
## ts-pattern Stack
- Version: ts-pattern >= 5.0
- Import: import { match, P } from "ts-pattern"
- Basic: match(value).with(pattern, handler).otherwise(fallback)
- Exhaustive: .exhaustive() — compile error if union case unhandled
- Guards: P.when(isString), P.when((x) => x > 0)
- Patterns: P._, P.string, P.number, P.boolean, P.union, P.array, P.instanceOf
- Select: P.select("key") / P.select() — extract values from pattern
- Infer: P.infer<typeof pattern> — TypeScript type of matched value
Basic Usage
// lib/order-processing.ts — pattern matching for business logic
import { match, P } from "ts-pattern"
type OrderStatus =
| "pending"
| "processing"
| "shipped"
| "delivered"
| "cancelled"
// Replaces switch statement with exhaustive checking
export function getOrderStatusLabel(status: OrderStatus): string {
return match(status)
.with("pending", () => "Awaiting payment")
.with("processing", () => "Preparing your order")
.with("shipped", () => "On the way!")
.with("delivered", () => "Delivered")
.with("cancelled", () => "Cancelled")
.exhaustive()
// TypeScript error if any case is missing — unlike switch
}
// Action allowability based on status
export function getAllowedActions(status: OrderStatus): string[] {
return match(status)
.with("pending", () => ["pay", "cancel"])
.with("processing", () => ["cancel"])
.with("shipped", () => ["track"])
.with("delivered", () => ["return", "review"])
.with("cancelled", () => [])
.exhaustive()
}
Discriminated Unions
// lib/result.ts — discriminated union result type
type Success<T> = { type: "success"; data: T }
type Failure = { type: "failure"; error: string; code: number }
type Pending = { type: "pending" }
type Result<T> = Success<T> | Failure | Pending
interface Order {
id: string
status: OrderStatus
totalCents: number
}
export function handleOrderResult(result: Result<Order>): string {
return match(result)
.with({ type: "success", data: P.select() }, (order) => {
// 'order' is typed as Order — extracted via P.select()
return `Order ${order.id} — $${(order.totalCents / 100).toFixed(2)}`
})
.with({ type: "failure", code: P.union(401, 403) }, ({ error }) => {
return `Authentication error: ${error}`
})
.with({ type: "failure", code: P.when(c => c >= 500) }, ({ error, code }) => {
return `Server error ${code}: ${error}`
})
.with({ type: "failure" }, ({ error }) => {
return `Error: ${error}`
})
.with({ type: "pending" }, () => "Loading...")
.exhaustive()
}
Event Handling / State Machine
// lib/order-state-machine.ts — state machine with ts-pattern
import { match, P } from "ts-pattern"
type OrderState =
| { status: "idle" }
| { status: "creating"; customerId: string }
| { status: "payment_pending"; orderId: string; totalCents: number }
| { status: "confirmed"; orderId: string }
| { status: "failed"; error: string }
type OrderAction =
| { type: "START_ORDER"; customerId: string }
| { type: "ORDER_CREATED"; orderId: string; totalCents: number }
| { type: "PAYMENT_SUCCEEDED" }
| { type: "PAYMENT_FAILED"; error: string }
| { type: "RESET" }
export function orderReducer(state: OrderState, action: OrderAction): OrderState {
return match({ state, action })
// Start placing an order
.with(
{ state: { status: "idle" }, action: { type: "START_ORDER", customerId: P.select() } },
(customerId) => ({ status: "creating" as const, customerId })
)
// Order created in backend
.with(
{
state: { status: "creating" },
action: { type: "ORDER_CREATED", orderId: P.select("orderId"), totalCents: P.select("totalCents") },
},
({ orderId, totalCents }) => ({ status: "payment_pending" as const, orderId, totalCents })
)
// Payment succeeded
.with(
{
state: { status: "payment_pending", orderId: P.select() },
action: { type: "PAYMENT_SUCCEEDED" },
},
(orderId) => ({ status: "confirmed" as const, orderId })
)
// Payment failed
.with(
{
state: { status: "payment_pending" },
action: { type: "PAYMENT_FAILED", error: P.select() },
},
(error) => ({ status: "failed" as const, error })
)
// Reset from any state
.with(
{ action: { type: "RESET" } },
() => ({ status: "idle" as const })
)
// Invalid transitions: stay in current state
.otherwise(({ state }) => state)
}
API Route Handlers
// routes/orders.ts — Express handlers with pattern matching
import { match, P } from "ts-pattern"
import type { Request, Response } from "express"
import { ZodError } from "zod"
import { DatabaseError } from "pg"
type HandlerError =
| { type: "validation"; error: ZodError }
| { type: "not_found"; id: string }
| { type: "unauthorized" }
| { type: "database"; error: DatabaseError }
| { type: "unknown"; error: unknown }
function sendErrorResponse(res: Response, err: HandlerError): void {
match(err)
.with({ type: "validation", error: P.select() }, (zodError) => {
res.status(422).json({
error: "Validation failed",
issues: zodError.flatten().fieldErrors,
})
})
.with({ type: "not_found", id: P.select() }, (id) => {
res.status(404).json({ error: `Resource ${id} not found` })
})
.with({ type: "unauthorized" }, () => {
res.status(401).json({ error: "Authentication required" })
})
.with({ type: "database", error: P.select() }, (dbError) => {
console.error("Database error:", dbError)
res.status(500).json({ error: "Database error" })
})
.with({ type: "unknown" }, () => {
res.status(500).json({ error: "Internal server error" })
})
.exhaustive()
}
export async function getOrder(req: Request, res: Response) {
try {
if (!req.user) {
return sendErrorResponse(res, { type: "unauthorized" })
}
const order = await db.orders.findById(req.params.id)
if (!order) {
return sendErrorResponse(res, { type: "not_found", id: req.params.id })
}
res.json(order)
} catch (err) {
if (err instanceof ZodError) {
sendErrorResponse(res, { type: "validation", error: err })
} else if (err instanceof DatabaseError) {
sendErrorResponse(res, { type: "database", error: err })
} else {
sendErrorResponse(res, { type: "unknown", error: err })
}
}
}
Complex Nested Patterns
// lib/webhook-handler.ts — webhook event routing
import { match, P } from "ts-pattern"
type WebhookEvent =
| { type: "order.created"; data: { orderId: string; customerId: string } }
| { type: "order.updated"; data: { orderId: string; status: string } }
| { type: "payment.succeeded"; data: { paymentIntentId: string; amount: number } }
| { type: "payment.failed"; data: { paymentIntentId: string; error: string } }
async function handleWebhookEvent(event: WebhookEvent) {
await match(event)
.with({ type: "order.created", data: P.select() }, async ({ orderId, customerId }) => {
await sendOrderConfirmationEmail(customerId, orderId)
})
.with(
{ type: "order.updated", data: { status: P.union("shipped", "delivered") } },
async ({ data: { orderId, status } }) => {
await sendOrderUpdateEmail(orderId, status)
}
)
.with(
{ type: "payment.succeeded", data: { amount: P.when(a => a >= 10000) } },
async ({ data }) => {
await notifyHighValuePayment(data.paymentIntentId, data.amount)
}
)
.with({ type: "payment.failed", data: P.select() }, async (data) => {
await handleFailedPayment(data.paymentIntentId, data.error)
})
.otherwise(async () => {
// Unhandled event type — log and ignore
console.log("Unhandled webhook event:", event.type)
})
}
For the Zod discriminated union alternative that handles schema validation with similar union discrimination but through runtime parsing rather than control flow pattern matching — better when the input is untrusted external data needing validation, see the Valibot guide for schema union patterns. For the Effect-TS alternative that provides pattern matching within a full functional programming runtime including error channels, dependency injection, and composable effects — more powerful but with a steeper learning curve, see the Effect TypeScript guide for the Effect model. The Claude Skills 360 bundle includes ts-pattern skill sets covering discriminated unions, exhaustive matching, and state machines. Start with the free tier to try ts-pattern match expression generation.