# Building matchers

A matcher is assembled with `pipe` in three stages: you **start** it, **add**
ordered cases, then **finish** it with a finalizer. Cases are evaluated top to
bottom and the first match wins, so order matters — put specific cases before
catch-alls. The type system narrows the handler argument for each case and
tracks the inputs that are still unmatched, which is what makes the
`Match.exhaustive` finalizer safe.

```ts
import { Match } from "effect"

type Shape =
  | { readonly _tag: "Circle"; readonly radius: number }
  | { readonly _tag: "Rect"; readonly width: number; readonly height: number }

// 1. start: Match.type<T>() produces a reusable (shape: Shape) => number
const area = Match.type<Shape>().pipe(
  // 2. add cases: shape is narrowed to the Circle member inside this branch
  Match.tag("Circle", (shape) => Math.PI * shape.radius ** 2),
  Match.tag("Rect", (shape) => shape.width * shape.height),
  // 3. finish: drop a tag above and this line stops compiling
  Match.exhaustive
)

console.log(area({ _tag: "Circle", radius: 2 })) // => 12.566...
console.log(area({ _tag: "Rect", width: 3, height: 4 })) // => 12
```
**Match is synchronous:** `Match` is a plain, synchronous module — it does not run inside `Effect`. It is
  the sibling of the `Filter` module: `Match` classifies a value and runs a
  handler, while `Filter` composes reusable accept/reject/transform predicates.

## Match by type

`Match.type<T>()` builds a *reusable* matcher: the result is a function you can
call with many different inputs. Use it when the same branch table is applied
in more than one place, or when you want exhaustiveness checking against a
union type.

```ts
import { Match } from "effect"

//      ┌─── (u: string | number) => string
//      ▼
const format = Match.type<string | number>().pipe(
  // Match.number / Match.string are built-in type refinements
  Match.when(Match.number, (n) => `number: ${n}`),
  Match.when(Match.string, (s) => `string: ${s}`),
  Match.exhaustive
)

console.log(format(0)) // => "number: 0"
console.log(format("hello")) // => "string: hello"
```

## Match by value

`Match.value(x)` classifies a single value right away. The matcher already
contains the input, so the finalizer returns the *result* directly instead of a
function. Use it for one-off branching where you don't need a reusable matcher.

```ts
import { Match } from "effect"

const input = { name: "John", age: 30 }

const result = Match.value(input).pipe(
  // Object patterns match by comparing the listed fields
  Match.when({ name: "John" }, (user) => `${user.name} is ${user.age}`),
  // orElse supplies a fallback, so the match need not be exhaustive
  Match.orElse(() => "not John")
)

console.log(result) // => "John is 30"
```

## Flat tag dispatch

When all you need is a `switch`-like table over a discriminated union's `_tag`,
the `*Tags` constructors skip the `pipe`/finalizer ceremony entirely. They take
an object mapping each tag to a handler and dispatch directly. There are three
flavours depending on whether you want an immediate result, a reusable
function, or a pipeline finalizer.

`Match.valueTags` matches **one value immediately** and returns the handler's
result directly — no finalizer, no function to call later.

```ts
import { Match } from "effect"

type Status =
  | { readonly _tag: "Success"; readonly data: string }
  | { readonly _tag: "Failure"; readonly error: string }

const success: Status = { _tag: "Success", data: "Hello" }

const message = Match.valueTags(success, {
  Success: (s) => `ok: ${s.data}`,
  Failure: (f) => `err: ${f.error}`
})

console.log(message) // => "ok: Hello"
```

`Match.typeTags<I>()` builds a **reusable function** from the same handler map.
It has two overloads: pass a return type (`typeTags<I, Ret>()`) to force every
branch to return `Ret`, or omit it (`typeTags<I>()`) to infer the union of all
branch results.

```ts
import { Match } from "effect"

type Result =
  | { readonly _tag: "Success"; readonly data: string }
  | { readonly _tag: "Error"; readonly message: string }
  | { readonly _tag: "Loading" }

// Pinned return type: every branch must return a string
const formatResult = Match.typeTags<Result, string>()({
  Success: (r) => `Data: ${r.data}`,
  Error: (r) => `Error: ${r.message}`,
  Loading: () => "Loading..."
})

console.log(formatResult({ _tag: "Success", data: "Hi" })) // => "Data: Hi"

// Inferred return type: the union of all branch results
const processResult = Match.typeTags<Result>()({
  Success: (r) => ({ type: "ok", value: r.data }),
  Error: (r) => ({ type: "error", error: r.message }),
  Loading: () => ({ type: "pending" })
})

console.log(processResult({ _tag: "Loading" })) // => { type: "pending" }
```

The three tag-dispatch entry points are easy to confuse:

| API                                | Shape                                       | Result                          |
| ---------------------------------- | ------------------------------------------- | ------------------------------- |
| `Match.valueTags(input, handlers)` | takes the value, immediate                  | the handler's return value      |
| `Match.typeTags<I>()(handlers)`    | reusable                                    | `(input: I) => Ret`             |
| `Match.tagsExhaustive(handlers)`   | a pipeline step (after `Match.type`/`value`) | finalizes the matcher (see below) |

## Defining cases with `when`

`Match.when` is the general-purpose case. Its first argument is a *pattern*,
which can be a literal value, a predicate function, or an object whose fields
are themselves patterns. The handler runs with the input narrowed to whatever
the pattern accepts.

```ts
import { Match } from "effect"

const classify = Match.type<{ age: number }>().pipe(
  // Field pattern with a predicate: matches when age > 18
  Match.when({ age: (age: number) => age > 18 }, (user) => `adult: ${user.age}`),
  // Field pattern with a literal: matches when age is exactly 18
  Match.when({ age: 18 }, () => "just eligible"),
  Match.orElse((user) => `minor: ${user.age}`)
)

console.log(classify({ age: 20 })) // => "adult: 20"
console.log(classify({ age: 18 })) // => "just eligible"
console.log(classify({ age: 4 })) // => "minor: 4"
```

### `Match.whenOr`

Runs the handler if **any** of the supplied patterns match. The handler's input
is narrowed to the union of the patterns.

```ts
import { Match } from "effect"

type ErrorType =
  | { readonly _tag: "NetworkError" }
  | { readonly _tag: "TimeoutError" }
  | { readonly _tag: "ValidationError"; readonly field: string }

const handle = Match.type<ErrorType>().pipe(
  Match.whenOr(
    { _tag: "NetworkError" },
    { _tag: "TimeoutError" },
    () => "Retry the request"
  ),
  Match.when({ _tag: "ValidationError" }, (e) => `Invalid: ${e.field}`),
  Match.exhaustive
)

console.log(handle({ _tag: "NetworkError" })) // => "Retry the request"
console.log(handle({ _tag: "ValidationError", field: "email" })) // => "Invalid: email"
```

### `Match.whenAnd`

Runs the handler only if **all** supplied patterns match. The patterns are
intersected, so the handler sees a value satisfying every one.

```ts
import { Match } from "effect"

type User = { readonly age: number; readonly role: "admin" | "user" }

const check = Match.type<User>().pipe(
  Match.whenAnd(
    { age: (n: number) => n >= 18 },
    { role: "admin" },
    () => "Admin access granted"
  ),
  Match.orElse(() => "Access denied")
)

console.log(check({ age: 20, role: "admin" })) // => "Admin access granted"
console.log(check({ age: 20, role: "user" })) // => "Access denied"
```

### `Match.not`

The inverse of `when`: it matches every input *except* those covered by the
pattern. Excluded values bypass the handler and continue through later cases.

```ts
import { Match } from "effect"

const greet = Match.type<string | number>().pipe(
  // Matches anything that is not the literal "hi"
  Match.not("hi", () => "ok"),
  Match.orElse(() => "fallback")
)

console.log(greet("hello")) // => "ok"
console.log(greet("hi")) // => "fallback"
```

## Built-in refinements

Patterns can use these refinements to match by type instead of by value. They
compose inside object patterns too (e.g. `{ a: Match.number }`). Each is shown
with a short example below.

| Refinement                      | Matches                                                          |
| ------------------------------- | --------------------------------------------------------------- |
| `Match.string`                  | values of type `string`                                         |
| `Match.nonEmptyString`          | non-empty strings                                               |
| `Match.number`                  | values of type `number` (incl. `NaN`, infinities)              |
| `Match.boolean`                 | values of type `boolean`                                        |
| `Match.bigint`                  | values of type `bigint`                                         |
| `Match.symbol`                  | values of type `symbol`                                         |
| `Match.date`                    | instances of `Date`                                             |
| `Match.record`                  | non-null, non-array objects                                     |
| `Match.null`                    | the literal `null`                                              |
| `Match.undefined`               | the literal `undefined`                                         |
| `Match.defined`                 | any non-null, non-undefined value                               |
| `Match.any`                     | any value, with no narrowing                                    |
| `Match.is(...values)`           | one of the given literal values, e.g. `Match.is("a", 42, true)` |
| `Match.instanceOf(Class)`       | instances of a class, with type-safe narrowing                  |
| `Match.instanceOfUnsafe(Class)` | instances of a class, without the narrowing guarantee           |
**Match.null, Match.undefined, and Match.defined:** `Match.null` and `Match.undefined` match the literals `null` and `undefined`
  respectively, while `Match.defined` matches any value that is neither. You can
  also match these literals with `Match.is(null)` / `Match.is(undefined)`.

### `Match.string`

Refines `unknown` to `string`.

```ts
import { Match } from "effect"

const f = Match.type<unknown>().pipe(
  Match.when(Match.string, (s) => s.toUpperCase()),
  Match.orElse(() => "not a string")
)

console.log(f("hi")) // => "HI"
console.log(f(42)) // => "not a string"
```

### `Match.nonEmptyString`

Matches strings of length greater than zero (whitespace-only strings still
count as non-empty).

```ts
import { Match } from "effect"

const f = Match.type<string>().pipe(
  Match.when(Match.nonEmptyString, (s) => `valid: ${s}`),
  Match.orElse(() => "empty")
)

console.log(f("hello")) // => "valid: hello"
console.log(f("")) // => "empty"
```

### `Match.number`

Refines `unknown` to `number`, including `NaN`, `Infinity`, and `-Infinity`.

```ts
import { Match } from "effect"

const f = Match.type<unknown>().pipe(
  Match.when(Match.number, (n) => `number: ${n}`),
  Match.orElse(() => "not a number")
)

console.log(f(3.14)) // => "number: 3.14"
console.log(f("3.14")) // => "not a number"
```

### `Match.boolean`

Refines `unknown` to the primitive `boolean` values `true` / `false`.

```ts
import { Match } from "effect"

const f = Match.type<unknown>().pipe(
  Match.when(Match.boolean, (b) => (b ? "yes" : "no")),
  Match.orElse(() => "not a boolean")
)

console.log(f(true)) // => "yes"
console.log(f(0)) // => "not a boolean"
```

### `Match.bigint`

Refines `unknown` to `bigint`.

```ts
import { Match } from "effect"

const f = Match.type<unknown>().pipe(
  Match.when(Match.bigint, (b) => `bigint: ${b}`),
  Match.orElse(() => "not a bigint")
)

console.log(f(123n)) // => "bigint: 123"
console.log(f(123)) // => "not a bigint"
```

### `Match.symbol`

Refines `unknown` to `symbol`.

```ts
import { Match } from "effect"

const f = Match.type<unknown>().pipe(
  Match.when(Match.symbol, (s) => `symbol: ${String(s)}`),
  Match.orElse(() => "not a symbol")
)

console.log(f(Symbol("x"))) // => "symbol: Symbol(x)"
console.log(f("x")) // => "not a symbol"
```

### `Match.date`

Matches `Date` instances only — not date strings or numeric timestamps.

```ts
import { Match } from "effect"

const f = Match.type<unknown>().pipe(
  Match.when(Match.date, (d) => d.getFullYear()),
  Match.orElse(() => "not a Date")
)

console.log(f(new Date("2024-01-01"))) // => 2024
console.log(f("2024-01-01")) // => "not a Date"
```

### `Match.record`

Backed by `Predicate.isObject`: matches non-null values whose runtime type is
`"object"` and that are not arrays. Note that `Date`, `RegExp`, and class
instances also pass — use `instanceOf` to distinguish them.

```ts
import { Match } from "effect"

const f = Match.type<unknown>().pipe(
  Match.when(Match.record, (obj) => Object.keys(obj).length),
  Match.orElse(() => "not a record")
)

console.log(f({ a: 1, b: 2 })) // => 2
console.log(f([1, 2, 3])) // => "not a record"
```

### `Match.defined`

Matches any value that is neither `null` nor `undefined`.

```ts
import { Match } from "effect"

const f = Match.type<string | null | undefined>().pipe(
  Match.when(Match.defined, (v) => `defined: ${v}`),
  Match.orElse(() => "nullish")
)

console.log(f("hi")) // => "defined: hi"
console.log(f(null)) // => "nullish"
```

### `Match.null`

Matches only the literal `null`. Backed by `Predicate.isNull`.

```ts
import { Match } from "effect"

const f = Match.type<string | null | undefined>().pipe(
  Match.when(Match.null, () => "was null"),
  Match.orElse((v) => `other: ${v}`)
)

console.log(f(null)) // => "was null"
console.log(f("hi")) // => "other: hi"
```

### `Match.undefined`

Matches only the literal `undefined`. Backed by `Predicate.isUndefined`.

```ts
import { Match } from "effect"

const f = Match.type<string | null | undefined>().pipe(
  Match.when(Match.undefined, () => "was undefined"),
  Match.orElse((v) => `other: ${v}`)
)

console.log(f(undefined)) // => "was undefined"
console.log(f("hi")) // => "other: hi"
```

### `Match.any`

Matches every input with no narrowing — including `null`, `undefined`, objects,
and functions. Put it last, since the first matching case wins.

```ts
import { Match } from "effect"

const f = Match.type<unknown>().pipe(
  Match.when(Match.string, (s) => `string: ${s}`),
  Match.when(Match.any, (v) => `other: ${typeof v}`),
  Match.exhaustive
)

console.log(f("hi")) // => "string: hi"
console.log(f(null)) // => "other: object"
```

### `Match.is`

Matches one of a set of literal primitive (or `null`) values.

```ts
import { Match } from "effect"

const f = Match.type<string | number>().pipe(
  Match.when(Match.is("ok", 200), () => "success"),
  Match.when(Match.is("error", 500), () => "failure"),
  Match.orElse((v) => `unknown: ${v}`)
)

console.log(f(200)) // => "success"
console.log(f("error")) // => "failure"
console.log(f("pending")) // => "unknown: pending"
```

### `Match.instanceOf`

Matches instances of a constructor and narrows the handler argument to the
instance type.

```ts
import { Match } from "effect"

const f = Match.type<unknown>().pipe(
  Match.when(Match.instanceOf(Map), (m) => `map size: ${m.size}`),
  Match.when(Match.instanceOf(Error), (e) => `error: ${e.message}`),
  Match.orElse(() => "other")
)

console.log(f(new Map([["a", 1]]))) // => "map size: 1"
console.log(f(new Error("boom"))) // => "error: boom"
```

### `Match.instanceOfUnsafe`

Same runtime `instanceof` check as `Match.instanceOf`, but it does **not**
remove the matched type from the remaining/unmatched set. Use it when you need
the looser refinement and are willing to narrow the handler argument manually;
prefer `Match.instanceOf` for normal type-safe matching.

```ts
import { Match } from "effect"

class CustomError extends Error {
  constructor(message: string, readonly code: number) {
    super(message)
  }
}

const f = Match.type<unknown>().pipe(
  Match.when(Match.instanceOfUnsafe(CustomError), (err) => {
    // narrowing is up to you here
    const e = err as CustomError
    return `code ${e.code}: ${e.message}`
  }),
  Match.orElse(() => "not a CustomError")
)

console.log(f(new CustomError("nope", 404))) // => "code 404: nope"
console.log(f("x")) // => "not a CustomError"
```

## Matching tagged unions

Effect's data types (errors, `Option`, `Result`, schema variants) all use a
`_tag` field as their discriminator. The `tag` family matches against it
directly and narrows the handler to that member.

### `Match.tag`

Matches one or more `_tag` values with a single handler.

```ts
import { Match } from "effect"

type RemoteData =
  | { readonly _tag: "Loading" }
  | { readonly _tag: "Success"; readonly data: string }
  | { readonly _tag: "Failure"; readonly error: Error }
  | { readonly _tag: "Cancelled" }

const render = Match.type<RemoteData>().pipe(
  // One handler can cover multiple tags
  Match.tag("Loading", "Cancelled", () => "…"),
  Match.tag("Success", (e) => `data: ${e.data}`),
  Match.tag("Failure", (e) => `error: ${e.error.message}`),
  Match.exhaustive
)

console.log(render({ _tag: "Success", data: "hi" })) // => "data: hi"
console.log(render({ _tag: "Loading" })) // => "…"
```

### `Match.tagStartsWith`

Matches any member whose `_tag` begins with a given prefix — handy for
hierarchical or namespaced tags like `"A.A"`.

```ts
import { Match } from "effect"

const f = Match.type<{ _tag: "A" } | { _tag: "B" } | { _tag: "A.A" }>().pipe(
  Match.tagStartsWith("A", () => 1 as const),
  Match.tagStartsWith("B", () => 2 as const),
  Match.orElse(() => 3 as const)
)

console.log(f({ _tag: "A" })) // => 1
console.log(f({ _tag: "A.A" })) // => 1
console.log(f({ _tag: "B" })) // => 2
```

### `Match.tags`

Takes an object mapping each `_tag` to a handler. It does **not** finalize the
matcher — handlers may be partial, and the pipeline continues, so you still need
a finalizer afterwards.

```ts
import { Match } from "effect"

type RemoteData =
  | { readonly _tag: "Loading" }
  | { readonly _tag: "Success"; readonly data: string }
  | { readonly _tag: "Failure"; readonly error: Error }

const render = Match.type<RemoteData>().pipe(
  // Handle only some tags here…
  Match.tags({
    Success: (e) => `data: ${e.data}`,
    Failure: (e) => `error: ${e.error.message}`
  }),
  // …then close the remaining ones
  Match.orElse(() => "…")
)

console.log(render({ _tag: "Loading" })) // => "…"
```

### `Match.tagsExhaustive`

The finalizing variant of `Match.tags`: every `_tag` must have a handler, so it
closes the matcher directly — no `Match.exhaustive` needed.

```ts
import { Match } from "effect"

type RemoteData =
  | { readonly _tag: "Loading" }
  | { readonly _tag: "Success"; readonly data: string }
  | { readonly _tag: "Failure"; readonly error: Error }

const render = Match.type<RemoteData>().pipe(
  // Omit a tag and this stops compiling
  Match.tagsExhaustive({
    Loading: () => "…",
    Success: (e) => `data: ${e.data}`,
    Failure: (e) => `error: ${e.error.message}`
  })
)

console.log(render({ _tag: "Failure", error: new Error("boom") }))
// => "error: boom"
```

## Matching other discriminators

The `discriminator` family mirrors the `tag` family exactly, but for any field
you name instead of the hard-wired `_tag`. Use it for unions discriminated by
`type`, `kind`, `status`, and so on.

### `Match.discriminator`

Matches one or more exact values of a named discriminator field.

```ts
import { Match } from "effect"

type Action =
  | { readonly type: "increment"; readonly by: number }
  | { readonly type: "decrement"; readonly by: number }
  | { readonly type: "reset" }

const reduce = Match.type<Action>().pipe(
  Match.discriminator("type")("increment", "decrement", (a) => a.by),
  Match.discriminator("type")("reset", () => 0),
  Match.exhaustive
)

console.log(reduce({ type: "increment", by: 5 })) // => 5
console.log(reduce({ type: "reset" })) // => 0
```

### `Match.discriminatorStartsWith`

Matches members whose discriminator field starts with a prefix — the
`tagStartsWith` equivalent for arbitrary fields.

```ts
import { Match } from "effect"

const f = Match.type<{ type: "A" } | { type: "B" } | { type: "A.A" }>().pipe(
  Match.discriminatorStartsWith("type")("A", () => 1 as const),
  Match.discriminatorStartsWith("type")("B", () => 2 as const),
  Match.orElse(() => 3 as const)
)

console.log(f({ type: "A" })) // => 1
console.log(f({ type: "A.A" })) // => 1
console.log(f({ type: "B" })) // => 2
```

### `Match.discriminators`

An object map of discriminator value to handler, the non-finalizing counterpart
of `Match.discriminator` (and the arbitrary-field version of `Match.tags`).

```ts
import { Match } from "effect"

type Shape =
  | { readonly kind: "circle"; readonly r: number }
  | { readonly kind: "square"; readonly side: number }

const area = Match.type<Shape>().pipe(
  Match.discriminators("kind")({
    circle: (c) => Math.PI * c.r ** 2,
    square: (s) => s.side ** 2
  }),
  Match.orElse(() => 0)
)

console.log(area({ kind: "square", side: 3 })) // => 9
```

### `Match.discriminatorsExhaustive`

The finalizing object-map variant: every discriminator value must be handled, so
the matcher is closed directly (the arbitrary-field version of
`Match.tagsExhaustive`).

```ts
import { Match } from "effect"

type Shape =
  | { readonly kind: "circle"; readonly r: number }
  | { readonly kind: "square"; readonly side: number }

const area = Match.type<Shape>().pipe(
  Match.discriminatorsExhaustive("kind")({
    circle: (c) => Math.PI * c.r ** 2,
    square: (s) => s.side ** 2
  })
)

console.log(area({ kind: "circle", r: 1 })) // => 3.14159...
```

## Finishing a matcher

Every matcher pipeline must be closed with a finalizer. The choice encodes what
should happen when no case matches. (The `*Exhaustive` and `*Tags` helpers above
finalize on their own.)

### `Match.exhaustive`

Compiles only when every possible input is handled, then produces the matcher
function (or, for `Match.value`, the result). Throws at runtime if narrowing was
somehow bypassed. Use it for closed unions where a missed case is a bug.

```ts
import { Match } from "effect"

const f = Match.type<"a" | "b">().pipe(
  Match.when("a", () => 1),
  Match.when("b", () => 2),
  Match.exhaustive
)

console.log(f("a")) // => 1
```

### `Match.orElse`

Runs a fallback handler for any unmatched input, so the match need not be
exhaustive. The handler receives the remaining (unmatched) input.

```ts
import { Match } from "effect"

const f = Match.type<string | number>().pipe(
  Match.when("a", () => "ok"),
  Match.orElse((v) => `fallback: ${v}`)
)

console.log(f("a")) // => "ok"
console.log(f("b")) // => "fallback: b"
```

### `Match.orElseAbsurd`

Finalizes a matcher that you believe handles every case, **without** requiring a
fallback handler or compile-time exhaustiveness proof: if an unmatched value
reaches it at runtime, it throws. Unlike `Match.exhaustive`, it does not require
the type system to have proven the union empty — so reach for `exhaustive` when
you want the compiler to catch the missing case, and `orElseAbsurd` only when an
unmatched input is genuinely impossible.

```ts
import { Match } from "effect"

const f = Match.type<"a" | "b">().pipe(
  Match.when("a", () => "Found A"),
  Match.when("b", () => "Found B"),
  Match.orElseAbsurd
)

console.log(f("a")) // => "Found A"
// f("c" as any) // throws at runtime
```

### `Match.option`

Returns the result as an [`Option`](https://effect.plants.sh/data-types/option/): `Option.some(value)`
on a match, `Option.none()` otherwise.

```ts
import { Match } from "effect"

type User = { readonly role: "admin" | "editor" | "viewer" }

const access = Match.type<User>().pipe(
  Match.when({ role: "admin" }, () => "full access"),
  Match.when({ role: "editor" }, () => "can edit"),
  Match.option
)

console.log(access({ role: "admin" }))
// => { _id: 'Option', _tag: 'Some', value: 'full access' }
console.log(access({ role: "viewer" }))
// => { _id: 'Option', _tag: 'None' }
```

### `Match.result`

Returns a [`Result`](https://effect.plants.sh/data-types/result/): `Result.succeed(value)` on a match,
or a failure carrying the unmatched input. This is the v4 replacement for the v3
`Match.either` finalizer.

```ts
import { Match } from "effect"

type User = { readonly role: "admin" | "editor" | "viewer" }

const getRole = Match.type<User>().pipe(
  Match.when({ role: "admin" }, () => "full access"),
  Match.when({ role: "editor" }, () => "can edit"),
  Match.result
)

console.log(getRole({ role: "admin" }))
// => { _id: 'Result', _tag: 'Success', value: 'full access' }
console.log(getRole({ role: "viewer" }))
// => { _id: 'Result', _tag: 'Failure', failure: { role: 'viewer' } }
```

## Pinning the return type

By default the result type is the union of every branch's return type. Call
`Match.withReturnType<T>()` **first** to force every branch to return `T`, so a
branch that returns the wrong type is rejected where it is defined.

```ts
import { Match } from "effect"

const match = Match.type<{ a: number } | { b: string }>().pipe(
  // Must come first in the pipeline
  Match.withReturnType<string>(),
  // @ts-expect-error — a is a number, not a string
  Match.when({ a: Match.number }, (_) => _.a),
  Match.when({ b: Match.string }, (_) => _.b),
  Match.exhaustive
)
```
**Order in the pipeline:** `Match.withReturnType<T>()` only enforces the return type when it is the first
  step after `Match.type` / `Match.value`. Placed later, the earlier branches
  escape the check.

## Model types

A few exported types describe the matcher itself. You rarely name them by hand,
but they appear in inferred signatures and error messages:

- **`Matcher`** — the union of the two concrete matcher kinds.
- **`TypeMatcher`** — produced by `Match.type<T>()`; finalizes to a reusable
  function.
- **`ValueMatcher`** — produced by `Match.value(x)`; carries the input and
  finalizes to a result.
- **`Case`** — a single accumulated case, defined as `When | Not` (the positive
  and negative case shapes). `SafeRefinement` is the type behind the built-in
  refinements like `Match.string` and `Match.is`.