Claude Code for ts-pattern: Exhaustive Pattern Matching in TypeScript — Claude Skills 360 Blog
Blog / Backend / Claude Code for ts-pattern: Exhaustive Pattern Matching in TypeScript
Backend

Claude Code for ts-pattern: Exhaustive Pattern Matching in TypeScript

Published: March 7, 2027
Read time: 7 min read
By: Claude Skills 360

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.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free