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-matchingSKILL.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
- Automatic constructors - No manual factory functions
- Automatic $match - Exhaustive pattern matching built-in
- Automatic $is - Type-safe guards for each variant
- Type inference - Compiler knows all variants
- 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
...
Repository Stats
Stars10
Forks4