pattern-matching

from front-depiction/claude-setup

Reusable Claude Code configuration for Effect TypeScript projects with specialized agents and skills

10 stars4 forksUpdated Jan 19, 2026
npx skills add https://github.com/front-depiction/claude-setup --skill pattern-matching

SKILL.md

Effect Pattern Matching Skill

Use this skill when working with discriminated unions, ADTs, conditional logic, or any type that uses _tag discrimination. Pattern matching provides exhaustive, type-safe alternatives to imperative conditionals.

Core Philosophy

Pattern matching over imperative conditionals:

  • Exhaustive by default (compiler enforces all cases)
  • Type-safe refinement in each branch
  • Declarative, not imperative
  • Pipeline-friendly composition

Pattern 1: Data.TaggedEnum for ADTs

Use Data.TaggedEnum instead of manual tagged unions.

The Problem: Manual Tagged Unions

// ❌ WRONG - Manual tagged union
type WalletState =
  | { readonly _tag: "Disconnected" }
  | { readonly _tag: "Connecting" }
  | { readonly _tag: "Connected"; readonly address: string }
  | { readonly _tag: "Error"; readonly message: string }

// Manual constructors - verbose and error-prone
const disconnected = (): WalletState => ({ _tag: "Disconnected" })
const connecting = (): WalletState => ({ _tag: "Connecting" })
const connected = (address: string): WalletState =>
  ({ _tag: "Connected", address })
const error = (message: string): WalletState =>
  ({ _tag: "Error", message })

// No built-in pattern matching
// No type guards
// No exhaustiveness checking

The Solution: Data.TaggedEnum

// ✅ CORRECT - TaggedEnum with constructors + $match + $is
import { Data } from "effect"

type WalletState = Data.TaggedEnum<{
  Disconnected: {}
  Connecting: {}
  Connected: { readonly address: string }
  Error: { readonly message: string }
}>

const WalletState = Data.taggedEnum<WalletState>()

/**
 * WalletState now provides:
 * - WalletState.Disconnected() - Constructor
 * - WalletState.Connecting() - Constructor
 * - WalletState.Connected({ address }) - Constructor
 * - WalletState.Error({ message }) - Constructor
 * - WalletState.$match(state, { ... }) - Pattern matching
 * - WalletState.$is("Connected")(state) - Type guard
 */

// Usage
const state = WalletState.Connected({ address: "0x123" })

// Pattern match
const display = WalletState.$match(state, {
  Disconnected: () => "Please connect wallet",
  Connecting: () => "Connecting...",
  Connected: ({ address }) => `Connected: ${address}`,
  Error: ({ message }) => `Error: ${message}`
})

// Type guard
if (WalletState.$is("Connected")(state)) {
  console.log(state.address) // Type-safe access
}

Benefits of Data.TaggedEnum

  1. Automatic constructors - No manual factory functions
  2. Automatic $match - Exhaustive pattern matching built-in
  3. Automatic $is - Type-safe guards for each variant
  4. Type inference - Compiler knows all variants
  5. Compile-time exhaustiveness - Forget a case? Compiler error

When to Use Data.TaggedEnum

  • State machines: Connection states, loading states, workflow states
  • Domain events: UserLoggedIn, UserLoggedOut, SessionExpired
  • Command types: CreateUser, UpdateUser, DeleteUser
  • Result types: Success, Failure, Pending
  • Any discriminated union with multiple variants

Pattern 2: Avoid Effect.either + _tag Checks

Use Effect.match instead of Effect.either with manual tag checks.

The Problem: Effect.either with Manual Checks

// ❌ WRONG - Effect.either with manual _tag checks
import { Effect, Either, Data } from "effect"

declare const User: { name: string; id: string }
type User = typeof User

class NotFound extends Data.TaggedError("NotFound")<{
  readonly id: string
}> {}

const getUser = (id: string): Effect.Effect<User, NotFound> => Effect.fail(new NotFound({ id }))

const program = Effect.gen(function* () {
  const result = yield* Effect.either(getUser("123"))

  // Manual tag checking - not exhaustive
  if (result._tag === "Left") {
    console.error(`User not found: ${result.left.id}`)
    return null
  }

  return result.right
})

Problems:

  • Not exhaustive (could forget Right case)
  • Verbose and imperative
  • Breaks pipeline style
  • Manual unwrapping of Either

The Solution: Effect.match

// ✅ CORRECT - Effect.match for declarative error handling
import { Effect, Data } from "effect"

declare const User: { name: string; id: string }
type User = typeof User

class NotFound extends Data.TaggedError("NotFound")<{
  readonly id: string
}> {}

const getUser = (id: string): Effect.Effect<User, NotFound> => Effect.fail(new NotFound({ id }))

const program = getUser("123").pipe(
  Effect.match({
    onFailure: (error) => {
      console.error(`User not found: ${error.id}`)
      return null
    },
    onSuccess: (user) => user
  })
)

Benefits:

  • Exhaustive (must handle both cases)
  • Declarative and pipeline-friendly
  • No manual Either unwrapping
  • Type-safe refinement in each branch

Effect.match Variants

import { Effect, Cause } from "effect"

declare const effect: Effect.Effect<unknown, unknown, unknown>
declare function handleError(error: unknown): unknown
declare fun

...
Read full content

Repository Stats

Stars10
Forks4