import type { Exact } from "type-fest"; import type { HandlerMap, Matchable, Member, PatternKey, PatternReturns, RedundantFallback, UnaryFn, UniverseGate, } from "./matcher-shared.ts"; type Handlers = { [K in PatternKey]: UnaryFn, R>; }; // The fallback is a *second argument*, not a property of the handler map, // because its parameter is the remainder `Exclude>` and TypeScript // fixes a property's contextual type before it infers its sibling keys. A later // argument, by contrast, is contextually typed from inference on an earlier // one, so the split is what makes the remainder expressible at all. // See development/library.md. type Fallback = UnaryFn< Exclude>, R >; // A fallback is redundant once the handler map covers `T`. The guard is folded // into `Handled`'s own (self-referential) constraint so it is checked *after* // inference; a conditional in the fallback's parameter type is evaluated while // `Handled` is still its constraint and would reject context-sensitive partial // maps. That placement also fixes where the diagnostic lands: the constraint // failure is reported on the argument that inferred `Handled` (the handler // map), so the required property is spelled as the message instead of relying // on its position. See development/library.md. type MustBePartial = PatternKey extends keyof Handled ? RedundantFallback : unknown; // TypeScript does not apply the excess-property check to a generic constraint, // so `Exact` restores it for the generic forms: a handler map can otherwise // carry keys outside `T`. // Strict returns: one common `R`. Overload order is load-bearing: // #1 Handlers (first) -> the exhaustive form and the autocomplete popup // #2 Fallback (last) -> accepts a partial handler map plus a fallback interface PrimitiveUnionMatcherStrict { (handlers: Handlers & UniverseGate): UnaryFn; < R, Handled extends Exact>, Handled> & MustBePartial, >( handlers: Handled & Partial> & UniverseGate, fallback: Fallback & UniverseGate, ): UnaryFn; } // Widened returns: the union of every handler's return type. `P` is inferred // from the whole handler map, whose closed constraint supplies the // contextual/autocomplete type. interface PrimitiveUnionMatcherWidening {

, P>>( handlers: P & UniverseGate, ): UnaryFn>; < R, Handled extends Exact>, Handled> & MustBePartial, >( handlers: Handled & UniverseGate, fallback: Fallback & UniverseGate, ): UnaryFn | R>; } const dispatch = (handlers: HandlerMap, fallback?: UnaryFn) => (shape: Matchable): unknown => // `handlers[true]` already coerces to the `"true"` property at runtime, // identical to `handlers[String(shape)]`, so indexing with `shape` // directly is sound: `shape` is a facade-checked universe member and // `PatternKey` only ever produces valid property keys. The assertion is // needed solely because TypeScript forbids indexing with // `boolean`/`null`/`undefined` (TS2538); it buys the number fast path. ( handlers[ // oxlint-disable-next-line typescript/no-unsafe-type-assertion shape as string | number ] ?? fallback ?? (() => { throw new Error(`Unhandled shape: ${String(shape)}`); }) )( // oxlint-disable-next-line typescript/no-unsafe-type-assertion shape as never, ); /** * Create a matcher for a finite primitive universe, with one common return * type. * * Use it when the value itself is the union (`"yes" | "no"`) and every handler * returns the same type. * * The returned builder takes a handler map keyed by the members; supplying a * second fallback argument allows a partial map and receives the unhandled * remainder. * * @typeParam T - The finite universe of primitive members to match. * @returns A builder for the handler map, or the handler map plus a fallback. * @example * const matchAnswer = getPrimitiveUnionMatcher<"yes" | "no">(); * const describe = matchAnswer({ * yes: () => "agreed", * no: () => "declined", * }); * describe("yes"); // "agreed" */ export const getPrimitiveUnionMatcher = < T extends Matchable, >(): PrimitiveUnionMatcherStrict => dispatch; /** * Create a matcher for a finite primitive universe whose return type is the * union of every handler's return type. * * Use it when the value itself is the union (`"yes" | "no"`) and the handlers * return different types. The `W` (widening) counterpart of * {@link getPrimitiveUnionMatcher}; the universe constraint and the optional * fallback are identical. * * @typeParam T - The finite universe of primitive members to match. * @returns A builder for the handler map, or the handler map plus a fallback. * @example * const matchReply = getPrimitiveUnionMatcherW<"yes" | "no">(); * const reply = matchReply({ * yes: () => 1, * no: () => "declined", * }); * // reply: (shape: "yes" | "no") => number | string */ export const getPrimitiveUnionMatcherW = < T extends Matchable, >(): PrimitiveUnionMatcherWidening => dispatch;