---
title: "Pattern matching"
description: "Pattern forms, as bindings, exhaustiveness rules, the structural record subset, and tuple scrutinees."
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.

# Pattern matching

`match` is an expression. It has no fallthrough, and its arms are checked for coverage and redundancy.

## Pattern forms

| Form | Spelling |
| --- | --- |
| Wildcard | `_` |
| Binding | `name` |
| Literal | `"paid"`, `42`, `true` |
| Constructor | `Paid({ method })`, `Ident(text)` |
| Array | `[first, ...rest]` |
| Record | `{ kind: "created", payload: { id } }` |
| Or-pattern | `LParen \| RParen` |
| `as` binding | `Paid(_) as p` |
| Guard | `Paid({ method }) if method == "ideal"` |

## `as` bindings

`pat as name` names the value that `pat` matched. It takes a single identifier and binds looser than
`|` (so `A | B as x` names whichever alternative matched). Root aliases and the supported
constructor-payload positions work in match arms and binding forms. A whole structural-record alias
also works in a match arm: it names the original object, narrowed by that arm's tests. The
structural-record subsection below lists the remaining nested-field restrictions.

An `as` binding is transparent to exhaustiveness and redundancy checking, since it matches exactly when its pattern matches. It lowers to a `const` in the arm or binding prologue, bound to the matched position:

| Matched position | What the alias binds |
| --- | --- |
| Arm root | The whole scrutinee |
| Array or record element | The indexed or property child |
| Single positional payload | The `value` field |
| Record payload | The narrowed variant object itself, `_tag` included |

## Exhaustiveness

Coverage is checked for Hem variants, `boolean`, closed literal unions, and `?T`. Those four are closed sets the compiler can enumerate, so a match over one needs no catch-all.

Record patterns are not among them. Structural object space is open — a value can always carry fields the pattern does not name — so a match whose arms are record patterns needs a final `_` even when the union's discriminant is a literal type and the arms look complete. Omitting it is HEM2005.

The specification's §7 states the broader rule, extending coverage to any discriminated union with literal discriminants, imported ones included. That is the intended destination; this page describes what the compiler does today.

Missing coverage, an open set with no catch-all, and redundant arms all report HEM2005. Unreachable arms warn.

Emit is a `switch` on the detected discriminant when the match is flat. A `default: throw` appears only where the Hem frontend proved coverage that TypeScript cannot.

## Structural record patterns

A record arm matches ordinary API, database, and event objects without converting them into Hem variants. The supported subset is deliberately narrow.

Permitted:

- Record alternatives at arm and field level, binding the same names
- Literal fields
- Punned or explicitly bound fields
- Wildcards
- Nested record patterns built from the same pieces
- An `as` alias on the whole arm pattern or a nested matched position

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"
}
```

Emit is an `if`/`else if` chain with plain `const` field bindings.

Project compilation checks finite literal-discriminant unions, including imported unions. An unguarded record pattern covers a whole variant when its discriminator matches and its remaining fields are unrestricted bindings or wildcards. Binding an optional field reads its possibly absent value; it does not test presence. Record alternatives and literal alternatives within the discriminator contribute coverage. Runtime guards, payload literals, and nested record restrictions remain partial; this bounded check does not prove arbitrary predicates.

```hem
type Result = { success: true, data: number } | { success: false, error: string }
let describe = (result: Result): string => match result {
| { success: true, data } => data.toFixed(2)
| { success: false, error } => error
}
```

Without a fallback, adding an uncovered variant produces HEM2005. An intentional final fallback is legal on a closed union and warns if currently unreachable; open domains still require it. Checker-free single-file compilation cannot establish the closed domain and requires a fallback. No new pattern syntax is involved.

```hem
type Change = { kind: "created", id: number } | { kind: "deleted", reason: string }

let createdId = (change: Change): number => match change {
| { kind: "created" } as created => created.id
| _ => 0
}
```

`created` is the same object as `change`, not a reconstructed record. The scrutinee is evaluated
once, and the alias is available in the arm's guard and body.

Alternatives bind exactly the same names with checked types. The leftmost complete match supplies
the bindings. The guard runs once; if false, matching advances to the next arm, not another
alternative in the same arm. Constructor alternatives follow the same rule.

```hem
type Change = { kind: "created", id: number } | { kind: "deleted", previousId: number }
let changeId = (input: { change: Change }): number => match input {
| { change: { kind: "created", id } | { kind: "deleted", previousId: id } as selected } => id
| _ => 0
}
```

`selected` names the matched child object, preserving identity and readonly access. A non-final
fallback and nested array or constructor patterns remain outside the structural subset.

Guarded value-producing matches use a labeled block so a successful arm assigns the result and
exits. A false guard continues to the next arm. This also lets TypeScript see definite assignment;
the compiler does not silence an uninitialized result with a type assertion. A final unreachable
throw is emitted only when Hem has proved coverage that TypeScript cannot see.

## Tuple scrutinees

`match [a, b] { … }` matches two or more scrutinees at once. It is syntax, not a value: no runtime array is built. Minimum arity is two, since one element is just `match a`.

Each element evaluates once. An identifier or `typeof x` element stays inline; anything else lands in a temporary. Every arm is a fixed-length array pattern whose columns test independently, emitting a conjunctive `if`/`else` chain.

```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, constructor payloads, and per-column `as` bindings are unrestricted.

Exhaustiveness generalizes column by column and names the missing combinations, capped at three followed by a count. An all-wildcard arm is the catch-all.

HEM2019 refuses three shapes: a rest element (arms are fixed-length), an `as` binding over the whole array pattern (there is no tuple value to name), and an arity mismatch between an arm and the scrutinee.

## Related diagnostics

| Code | Meaning |
| --- | --- |
| HEM2005 | Missing coverage, open set without a catch-all, or a redundant arm |
| HEM2016 | Ambiguous pattern binding |
| HEM2019 | Tuple-scrutinee arm shape |
| HEM3003 | Pattern outside a supported subset |

## Normative source

Specification 0.2, §7 (control flow). The exhaustiveness and redundancy algorithm is specified alongside it.

Source: https://hem.run/reference/pattern-matching/index.mdx
