🐛 Gate the universe to finite, literal keys

An open universe (`string`, `number`, a template literal) makes the
handler map index-like, so a partial object satisfied the exhaustive
overload and the runtime `dispatch` throw was reachable. A universe
containing a member and its stringification (`"true" | true`, `1 | "1"`)
collapsed two members onto one key.

- reject broad universes and value/stringification collisions at the
  factory, with a diagnostic naming the reason
- invert `PatternKey` against the universe (`Member<T, K>`) instead of
  `PatternParam<K>`, so a standalone `"true"` is typed `"true"`
- intersect `UniverseGate<T>` into the handler and fallback parameters,
  preserving `R` inference and the popup
This commit is contained in:
tmu committed 2026-09-23 13:41:57 +00:00
1 parent 1723419e69
commit 3b5269f9e1
6 files changed
+953 -95

No files matched your search

+17 -13
View File
@@ -3,11 +3,12 @@ import type { Exact, UnknownRecord } from "type-fest";
import type {
HandlerMap,
Matchable,
Member,
PatternKey,
PatternParam,
PatternReturns,
RedundantFallback,
UnaryFn,
UniverseGate,
} from "./matcher-shared.ts";
// A tagged union is discriminated by one property whose values are the tags (a
@@ -17,8 +18,8 @@ import type {
// to write a handler under, and `bigint` is not a property key.
// The discriminant values of `T` under `K`. `Extract` keeps the finite literal
// tags and leaves a widened `string`/`number` as itself, so an open universe
// keeps an open fallback.
// tags and leaves a widened `string`/`number` as itself; a broad tag is then
// rejected by the universe gate.
type Tags<T extends object, K extends keyof T> = Extract<T[K], Matchable>;
// The keys of `T` that can act as a discriminant. `getTaggedUnionMatcher<T>()`
@@ -30,9 +31,10 @@ type Discriminated<T extends object> = {
// The member(s) of `T` tagged `V`. `Extract` distributes over the union, so a
// duplicated tag maps to a union of members rather than silently dropping one.
// Keyed by `PatternKey`, so `boolean`/`null`/`undefined` tags can key a mapped
// type; `PatternParam` inverts the projection to recover the member.
// type; `Member` inverts the projection against the tag set to recover the
// member.
type MapTaggedUnion<T extends object, K extends keyof T> = {
[P in PatternKey<Tags<T, K>>]: Extract<T, Record<K, PatternParam<P>>>;
[P in PatternKey<Tags<T, K>>]: Extract<T, Record<K, Member<Tags<T, K>, P>>>;
};
type Handlers<T extends object, K extends keyof T, R> = {
@@ -43,7 +45,7 @@ type Handlers<T extends object, K extends keyof T, R> = {
// index within `MapTaggedUnion`'s keys, and the conditional drops a stray key
// outside `T` so it cannot widen the remainder. The remainder is
// `Exclude<T, …>`, mirroring the primitive-union matcher's
// `Exclude<T, PatternParam<keyof Handled>>`.
// `Exclude<T, Member<T, keyof Handled>>`.
type HandledMembers<T extends object, K extends keyof T, Handled> = {
[P in PatternKey<Tags<T, K>>]: P extends keyof Handled
? MapTaggedUnion<T, K>[P]
@@ -51,7 +53,7 @@ type HandledMembers<T extends object, K extends keyof T, Handled> = {
}[PatternKey<Tags<T, K>>];
// The fallback is a *second argument*, not a property of the handler map, so
// its parameter can be the remainder the map left open. See development/library.md.
// its parameter can be the remainder the map left uncovered. See development/library.md.
type Fallback<T extends object, K extends keyof T, Handled, R> = UnaryFn<
Exclude<T, HandledMembers<T, K, Handled>>,
R
@@ -71,14 +73,16 @@ type MustBePartial<T extends object, K extends keyof T, Handled> =
// #1 Handlers (first) -> the exhaustive form and the autocomplete popup
// #2 Fallback (last) -> accepts a partial handler map plus a fallback
interface TaggedUnionMatcherStrict<T extends object, K extends keyof T> {
<R>(handlers: Handlers<T, K, R>): UnaryFn<T, R>;
<R>(handlers: Handlers<T, K, R> & UniverseGate<Tags<T, K>>): UnaryFn<T, R>;
<
R,
Handled extends Exact<Partial<Handlers<T, K, R>>, Handled> &
MustBePartial<T, K, Handled>,
>(
handlers: Handled & Partial<Handlers<T, K, R>>,
fallback: Fallback<T, K, Handled, R>,
handlers: Handled &
Partial<Handlers<T, K, R>> &
UniverseGate<Tags<T, K>>,
fallback: Fallback<T, K, Handled, R> & UniverseGate<Tags<T, K>>,
): UnaryFn<T, R>;
}
@@ -87,15 +91,15 @@ interface TaggedUnionMatcherStrict<T extends object, K extends keyof T> {
// contextual/autocomplete type.
interface TaggedUnionMatcherWidening<T extends object, K extends keyof T> {
<P extends Exact<Handlers<T, K, unknown>, P>>(
handlers: P,
handlers: P & UniverseGate<Tags<T, K>>,
): UnaryFn<T, PatternReturns<P>>;
<
R,
Handled extends Exact<Partial<Handlers<T, K, unknown>>, Handled> &
MustBePartial<T, K, Handled>,
>(
handlers: Handled,
fallback: Fallback<T, K, Handled, R>,
handlers: Handled & UniverseGate<Tags<T, K>>,
fallback: Fallback<T, K, Handled, R> & UniverseGate<Tags<T, K>>,
): UnaryFn<T, PatternReturns<Handled> | R>;
}