# Library design The type-level design of the public API and the limitations it carries. The user-facing reference is [README § API](../README.md#api). The matcher below is implemented in `src/primitive.ts` and re-exported from `src/index.ts` as `getMatcher` / `getMatcherW`; the rest of the library is placeholder code. ## Matcher shape #### Decision (2026-09) A matcher is built by a factory and applied to a pattern: ```ts const matcher = getMatcher<"a" | "b">()({ a: (s) => …, b: (s) => … }); ``` Whether the pattern is exhaustive or has a fallback is decided **at the call site**, by whether it carries `_` — F#'s `| _ ->`. Only the return-strictness axis remains, so there are two factories: - `getMatcher` — one common `R`, the best common return type of every handler; - `getMatcherW` — the union of every handler's return type. Both are three overloads whose order is load-bearing: 1. `ExhaustiveLoose` = `{ [K in T]: UnaryFn } & { _?: UnaryFn }` 2. `Fallback` = `Partial<…> & { _: UnaryFn }` 3. `Handlers` — the pure exhaustive shape #### Why - **Autocomplete reads the first overload, the error reads the last.** TypeScript takes the first overload signature as the contextual type for the object-literal popup, and the last for `No overload matches this call`. So `ExhaustiveLoose` first yields the popup `_?, a, b` (T-keys required, `_` optional) while `Handlers` last yields `Property 'b' is missing`. The two can be tuned independently. - Two factories, not four: the fallback is a pattern _shape_, not a separate API. - The widened return union is derived from the pattern's handler types, so it needs no fourth signature. #### Rejected - **Four factories** (exhaustive and fallback each split by return handling). The exhaustive/fallback axis is expressible as one pattern type; four signatures duplicate it. - **Union merge** — one type `Exhaustive | (Partial<…> & { _: … })`, explicit `()`. Type-safe and completable, but TypeScript reports the near-miss union member, so a missing key reads `Property '_' is missing` instead of naming the key. Arm order does not change the report; the overload split does. - **Overload merge with only the exhaustive arm last.** Fixes the missing-key message, but a wrong `_` parameter is then reported against the exhaustive arm, and `Parameters` sees only one arm. - **Inferred universe** — `match(pattern)` with `T` taken from the keys (exhaustive) or from `_`'s annotated parameter (fallback), via `NoInfer` and `_?: never`, split by overloads (a plain union merges inference; measured `T = "_" | "a"`). No explicit ``, and pipe-friendly. Rejected because: with no declared universe the exhaustive popup offers only `_`; an unannotated `_` widens `T` to `string | number`; and `NoInfer` leaks into the emitted `.d.ts`, raising the consumer floor to TypeScript 5.4 (README promises `>= 5.0`). - **Conditional `RequireKeys`** — parameter `P & ("_" extends keyof P ? unknown : Handlers)`. Gives the good missing-key message, but `keyof P` counts _optional_ keys: a widened value whose declared type has `_?:` bypasses the completeness check. Demanding a required `_` instead rejects that case but breaks `P` inference — `P` falls back to its constraint and partial literals then demand every key. Typos also need a `NoExtra` guard, whose message degrades to `not assignable to never`. - **Cases-first curried** — `match(["a", "b"])({ a: …, b: … })`. Completion works for exhaustive patterns, and the array is a single source of truth for the runtime list and the union. Rejected as not pipe-friendly; it needs a runtime array; and the single-call form `match(cases, pattern)` cannot infer `R` (the mapped key type `K[number]` stays deferred, so `R` widens to `unknown`). #### Known issue - The `_` handler receives **all** of `T`, not the unhandled subset (`Exclude`). - An exhaustive pattern that also carries `_` is accepted; the redundant `_` should be a compile error. - The widened overloads carry a completeness guard `keyof P extends T | "_" ? unknown : never`, because TypeScript does not apply the excess-property check to a generic constraint: a generic parameter accepts extra keys, a parameter typed as a concrete object type does not. `PatternReturns` must be `ReturnType, (...args: never[]) => unknown>>` so it survives the closed, partly-optional `P` constraints. - `Parameters[0]` resolves only the **last** overload, so it is not a sound "rejected" oracle for a factory. Factory-negative tests use `@ts-expect-error` call sites (the test file only — the general ban stands).