Skip to content

match

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

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:

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:

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 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:

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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close