Drop the strict/widened aliases from src/index.ts so the factories are exported under their own names, and align the type tests, the autocomplete probe (which now imports the public surface from ./index.ts) and library.md with them. Also correct the widened-overload comment: the excess-property check does not apply to a generic P, so MatcherWidening's keyof guard — not the closed constraint — is what rejects keys outside T/_, as library.md already documented.
4.7 KiB
4.7 KiB
Library design
The type-level design of the public API and the limitations it carries. The user-facing reference is README § 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:
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 commonR, 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:
ExhaustiveLoose<R, T>={ [K in T]: UnaryFn<K, R> } & { _?: UnaryFn<T, R> }Fallback<R, T>=Partial<…> & { _: UnaryFn<T, R> }Handlers<R, T>— 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. SoExhaustiveLoosefirst yields the popup_?, a, b(T-keys required,_optional) whileHandlerslast yieldsProperty '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<R,T> | (Partial<…> & { _: … }), explicit<T>(). Type-safe and completable, but TypeScript reports the near-miss union member, so a missing key readsProperty '_' is missinginstead 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, andParameters<typeof factory>sees only one arm. - Inferred universe —
match(pattern)withTtaken from the keys (exhaustive) or from_'s annotated parameter (fallback), viaNoInfer<T>and_?: never, split by overloads (a plain union merges inference; measuredT = "_" | "a"). No explicit<T>, and pipe-friendly. Rejected because: with no declared universe the exhaustive popup offers only_; an unannotated_widensTtostring | number; andNoInferleaks into the emitted.d.ts, raising the consumer floor to TypeScript 5.4 (README promises>= 5.0). - Conditional
RequireKeys— parameterP & ("_" extends keyof P ? unknown : Handlers<R, T>). Gives the good missing-key message, butkeyof Pcounts optional keys: a widened value whose declared type has_?:bypasses the completeness check. Demanding a required_instead rejects that case but breaksPinference —Pfalls back to its constraint and partial literals then demand every key. Typos also need aNoExtraguard, whose message degrades tonot 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 formmatch(cases, pattern)cannot inferR(the mapped key typeK[number]stays deferred, soRwidens tounknown).
Known issue
- The
_handler receives all ofT, not the unhandled subset (Exclude<T, handledKeys>). - 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.PatternReturnsmust beReturnType<Extract<ValueOf<P>, (...args: never[]) => unknown>>so it survives the closed, partly-optionalPconstraints. Parameters<typeof factory>[0]resolves only the last overload, so it is not a sound "rejected" oracle for a factory. Factory-negative tests use@ts-expect-errorcall sites (the test file only — the general ban stands).