# 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). ## A type guard is the single primitive #### Decision (2026-09) Every pattern constructor returns a `Matcher` whose only member is `matches: (value: unknown) => value is T`; `Pattern` is an alias. #### Why - A type guard is the one TypeScript construct that both narrows in an `if` and composes into a chain, so the library needs no DSL and no transpiler. - Because `matches` narrows, `match(value).with(pattern, handler)` types the handler with no cast and no runtime tag. - `Pattern` is an alias, so either name is the same type. ## The builder is immutable #### Decision (2026-09) `.with` returns a new builder rather than mutating the current one. #### Why - A partially built chain can be stored and reused without one call site affecting another. - `.with` widens the result type to `R | V`, which cannot be represented by a value that changes type in place. ## `exhaustive()` is a runtime check #### Decision (2026-09) `.exhaustive()` throws when no case matched; it does not statically prove that every member of the input union has a case. #### Why - Proving coverage through a fluent chain would need a compiler plugin or a builder that tracks an uncovered-member set — out of proportion to this library's size. - The union of handler return types is still type-safe; only the chain's completeness is unchecked. #### Known issue - A missing case is a runtime `Error`, not a compile error. A caller wants either an `.otherwise(...)` (which never throws) or to prove coverage themselves. Tracking uncovered members is a possible future change. ## `P.type` takes the type as a parameter #### Decision (2026-09) `P.type(name)` requires the caller to supply `T`; it is not inferred from the `typeof` string. #### Why - A runtime string cannot carry a TypeScript type; the caller pairs the two, and the compiler only checks that `T` is used consistently afterwards. #### Known issue - `T` and the runtime `name` can disagree (`P.type("string")` compiles), because nothing ties them together. Prefer `P.when` with a real type guard. A name-to-type conditional mapping is a possible future change. ## `P.shape` narrows only through `refine` #### Decision (2026-09) `P.shape(shape, refine?)` returns `Matcher`, defaulting to `Matcher` without a `refine`. #### Why - The shape's values may be matchers, so `S` describes the check, not the value; the `refine` guard is the explicit point where the value narrows to `T`. - Keys are checked with `in` rather than requiring an exact match, so a shape can match a wider object — which is how discriminated unions are handled. #### Known issue - Without `refine` a discriminated-union case does not narrow, so `P.shape` alone is easy to misuse. Prefer `P.when` when a guard is already available. ## `P.any` exists for non-guard predicates #### Decision (2026-09) `P.any(predicate)` takes a boolean predicate and a declared `T`. #### Why - Some checks are cheap as a boolean but awkward as a type guard (for example an `every` over an array); `P.any` is the escape hatch. #### Known issue - As with `P.type`, the declared `T` is not proven by the predicate. Prefer `P.when` whenever the check can be written as a guard.