# 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); this file records why the API is shaped the way it is. ## 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 for `Matcher`. #### Why - A type guard is the one TypeScript construct that both narrows in an `if` and composes into a chain, so the whole library can be built from it without a DSL or transpiler. - Because `matches` narrows, `match(value).with(pattern, handler)` can give the handler the right type with no cast and no runtime type tag. - Keeping `Pattern` as an alias means a caller can name either; the type is identical. ## The builder is immutable #### Decision (2026-09) `match(value)` returns a builder whose `.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 `R` to `R | V`; a value that changes type in place cannot be represented soundly. A new value per `.with` is what makes the type-level accumulation work. ## `exhaustive()` is a runtime check #### Decision (2026-09) `.exhaustive()` throws at runtime when no case matched. It does not statically prove that every member of the input union has a case. #### Why - TypeScript cannot require a fluent call chain to cover every union member without a compiler plugin or a builder whose type tracks an uncovered-member set. That machinery is out of proportion to the library's size. - The union-of-return-types (`R`) is still type-safe; what is not guaranteed is that the chain is complete. #### Known issue - A missing case is a runtime `Error`, not a compile error. A caller who wants compile-time safety must either add a `.otherwise(...)` (which never throws) or prove coverage themselves. Making the builder track uncovered members is a possible future change; it is not in scope now. ## `P.type` takes the type as a parameter #### Decision (2026-09) `P.type(name)` requires the caller to supply `T` explicitly; 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 - The type parameter and the runtime name can disagree (`P.type("string")` compiles), because nothing ties `T` to `name`. `P.when` with a real type guard avoids the pairing entirely, so prefer it. A future revision could map the name to a type with a conditional type; that is not in scope now. ## `P.shape` narrows only through `refine` #### Decision (2026-09) `P.shape(shape, refine?)` returns `Matcher`, defaulting to `Matcher` when no `refine` is given. #### Why - The shape object's values may be matchers, so the shape's literal type is not the matched type: `S` describes the check, not the value. A `refine` type guard is the explicit point where the value is narrowed to `T`. - Checking keys with `in` (rather than requiring exact keys) lets a shape 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` on its own is easy to misuse. The README shows the `refine` form; prefer `P.when` when a guard is already available. ## `P.any` exists for non-guard predicates #### Decision (2026-09) `P.any(predicate)` accepts a boolean predicate and a declared `T`. #### Why - Some checks are cheap to write as a boolean but awkward as a type guard (for example an `every` over an array). `P.any` is the escape hatch for those. #### Known issue - Like `P.type`, the declared `T` is not proven by the predicate. Prefer `P.when` whenever the check can be written as a guard.