🐛 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:
1 parent
1723419e69
commit
3b5269f9e1
6 files changed
+953
-95
No files matched your search
+17
-13
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user