---
title: "match"
image: "https://hem.run/social/hem.png"
---

> Documentation Index
> Fetch the complete documentation index at: https://hem.run/llms.txt
> Use this file to discover all available pages before exploring further.

# match

Hem replaces `switch` with `match`, an expression form that supports destructuring and guards without fallthrough:

```hem
let label = (s: PaymentState) =>
  match s {
  | Pending => "Awaiting payment"
  | Paid({ method }) if method == "ideal" => "Paid via iDEAL"
  | Paid(_) => "Paid"
  | Refunded({ reason }) => `Refunded: ${reason}`
  }
```

Patterns: wildcards `_`, bindings, literals, constructors, arrays `[a, ...rest]`, structural records in the subset below, or-patterns `LParen | RParen`, `as` bindings `Paid(_) as p` (keep the whole value while destructuring it), guards `if expr`. Exhaustiveness is checked for Hem variants, `boolean`, closed literal unions, and `?T`. The compiler diagnostic indicates missing cases:

```
error[HEM2005] billing.hem:31:34
   |
31 | let label = (s: PaymentState) => match s {
   |                                  ^^^^^^^^^ This match is missing `Refunded`. Add an explicit arm for each missing case.
```

When you need both the pieces and the whole, `as` names what a pattern just matched, at any depth. It binds looser than `|`, so `A | B as x` names whichever alternative hit:

```hem
match payment {
| Paid({ method } as receipt) => archive(receipt, method)
| Refunded(_) as r => audit(r)
| Pending => queue(payment)
}
```

Because a record payload's fields spread onto the variant (§7), `receipt` is the narrowed `Paid` value itself (`_tag` and all), not a detached payload object. The same pattern applies in `catch` blocks, allowing access to both destructured fields and the parent error instance (for example, `NetworkDown({ url }) as e`).

Matching isn't limited to Hem's own types. Any imported TS discriminated union works: the compiler finds the discriminant (`kind`, `type`, whatever the library chose) in the resolved type and switches on it. And when every discriminant value is a literal type, the union is a closed world and exhaustiveness is checked there too, third-party or not; a `_ =>` arm is only demanded when the union is genuinely open (a string-typed discriminant, say).

**Matching structural TypeScript records.** A record arm matches ordinary API, database, or event objects without changing them into Hem variants. The supported subset is deliberately small, and literal tests run before nested reads so TypeScript narrows a discriminated union before the arm binds its payload:

```hem
let describe = (event: Event): string => match event {
| { kind: "created", payload: { id } } => `created:${id}`
| { kind: "deleted", id } => `deleted:${id}`
| _ => "unknown"
}
```

This emits a readable `if`/`else if` chain and plain `const` field bindings. Structural object space is open, so an unguarded final `_` arm is required even when today's imported union looks closed; omitting it is HEM2005. Shapes outside the subset refuse with HEM3003 instead of guessing, and the [pattern matching reference](https://hem.run/reference/pattern-matching) lists which those are.

**Matching a tuple.** Write `match [a, b] { … }` to match two or more scrutinees at once: a state and an event, a pair of flags. It is syntax, not a value: no runtime tuple is built. Each element evaluates once (an identifier or `typeof x` element stays inline; anything else lands in a temporary), and every arm is a fixed-length array pattern whose columns test independently, so a transition table reads straight down the page:

```hem
let step = (state: State, event: Event): State =>
  match [state, event] {
  | [Idle, Start] => Loading
  | [Loading, Finish] => Ready
  | [Ready | Done, Reset] => Idle
  | [_, _] => state
  }
```

Per-column or-patterns (`Ready | Done`), constructor payloads, and per-column `as` all behave as they do on a single scrutinee. Exhaustiveness generalizes column by column: drop the `[_, _]` arm and the error names the missing *combinations* (`This match is missing [Loading, Reset], [Ready, Fail], [Failed, Start], and 4 more`, capped at three plus a count). Three shapes are refused (HEM2019), and the reference names them.

Source: https://hem.run/tour/match/index.mdx
