♻️ Share matcher internals

This commit is contained in:
tmu committed 2026-09-21 23:15:09 +00:00
1 parent 6f4f509b69
commit 937d84ea48
4 files changed
+63 -26

No files matched your search

+21
View File
@@ -93,6 +93,27 @@ Each factory is two overloads whose order is load-bearing:
not a sound "rejected" oracle for a factory. Factory-negative tests use
`@ts-expect-error` call sites (the test file only — the general ban stands).
## Shared internals
#### Decision (2026-09)
`src/matcher-shared.ts` holds the four universe-agnostic pieces both matchers
use: `UnaryFn`, `PatternReturns`, `RedundantFallback`, `HandlerMap`.
#### Why
- `RedundantFallback`'s property name is the diagnostic, so one definition
keeps the two matchers' message from drifting; the other three appear verbatim
in both public signatures.
#### Rejected
- **A generic `Matcher<Universe>` over the interface pair, `Handlers`,
`Fallback` and `MustBePartial`.** Each is built from its own universe
(`PatternKey`/`PatternParam` vs `Tags`/`MapTaggedUnion`); abstracting over the
F-bounded `Handled` constraint that makes the remainder work risks the
contextual typing it exists to preserve.
## Tagged-union matcher
#### Decision (2026-09)
+28
View File
@@ -0,0 +1,28 @@
import type { ValueOf } from "type-fest";
// The primitive and tagged-union matchers differ in their universe, but the
// handler/fallback plumbing is identical; these are the shared pieces. The
// boundary is deliberate: `Handlers`, `Fallback` and `MustBePartial` stay with
// each matcher because they are built from its universe. See development/library.md.
// A handler: one universe member in, one return value out.
export type UnaryFn<T, R> = (shape: T) => R;
// `Extract` drops optional handlers (`undefined`) so `PatternReturns` also works
// when `P`'s constraint has optional keys.
export type PatternReturns<P> = ReturnType<
Extract<ValueOf<P>, (...args: never[]) => unknown>
>;
// The diagnostic raised when a fallback is supplied for an already-exhaustive
// handler map. Each matcher's `MustBePartial` folds it into `Handled`'s
// constraint so the guard is checked after inference.
export interface RedundantFallback {
readonly "every case is already handled, so the fallback is redundant": never;
}
// The runtime dispatch map the handler maps and fallback erase to.
export type HandlerMap = Record<
string | number,
UnaryFn<never, unknown> | undefined
>;
+7 -13
View File
@@ -1,6 +1,11 @@
import type { Exact, ValueOf } from "type-fest";
import type { Exact } from "type-fest";
type UnaryFn<T, R> = (shape: T) => R;
import type {
HandlerMap,
PatternReturns,
RedundantFallback,
UnaryFn,
} from "./matcher-shared.ts";
// The primitive universe a matcher can discriminate. `boolean` is admitted as
// the pair `true | false`; see README § Caveats for the unsupported members.
@@ -29,12 +34,6 @@ type PatternParam<K> = K extends "true"
? undefined
: K;
// `Extract` drops optional handlers (`undefined`) so `PatternReturns` also works
// when `P`'s constraint has optional keys.
type PatternReturns<P> = ReturnType<
Extract<ValueOf<P>, (...args: never[]) => unknown>
>;
type Handlers<T extends Matchable, R> = {
[K in PatternKey<T>]: UnaryFn<PatternParam<K>, R>;
};
@@ -58,9 +57,6 @@ type Fallback<T extends Matchable, Handled, R> = UnaryFn<
// 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.
interface RedundantFallback {
readonly "every case is already handled, so the fallback is redundant": never;
}
type MustBePartial<T extends Matchable, Handled> =
PatternKey<T> extends keyof Handled ? RedundantFallback : unknown;
@@ -104,8 +100,6 @@ interface MatcherWidening<T extends Matchable> {
}
// oxlint-enable typescript/unified-signatures
type HandlerMap = Record<string | number, UnaryFn<never, unknown> | undefined>;
const dispatch =
(handlers: HandlerMap, fallback?: UnaryFn<never, unknown>) =>
(shape: Matchable): unknown =>
+7 -13
View File
@@ -1,6 +1,11 @@
import type { Exact, UnknownRecord, ValueOf } from "type-fest";
import type { Exact, UnknownRecord } from "type-fest";
type UnaryFn<T, R> = (shape: T) => R;
import type {
HandlerMap,
PatternReturns,
RedundantFallback,
UnaryFn,
} from "./matcher-shared.ts";
// A tagged union is discriminated by one property whose values are the tags.
// Only `string` and `number` tags can key a handler map: `symbol` has no
@@ -28,12 +33,6 @@ type Handlers<T extends object, K extends keyof T, R> = {
[V in Tags<T, K>]: UnaryFn<MapTaggedUnion<T, K>[V], R>;
};
// `Extract` drops optional handlers (`undefined`) so `PatternReturns` also works
// when `P`'s constraint has optional keys.
type PatternReturns<P> = ReturnType<
Extract<ValueOf<P>, (...args: never[]) => unknown>
>;
// The members `Handled` covers. Mapping over `Tags` keeps every 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
@@ -55,9 +54,6 @@ type Fallback<T extends object, K extends keyof T, Handled, R> = UnaryFn<
// into `Handled`'s own (self-referential) constraint so it is checked *after*
// inference; see the primitive matcher for why a conditional in the fallback's
// parameter is evaluated too early.
interface RedundantFallback {
readonly "every case is already handled, so the fallback is redundant": never;
}
type MustBePartial<T extends object, K extends keyof T, Handled> =
Tags<T, K> extends keyof Handled ? RedundantFallback : unknown;
@@ -96,8 +92,6 @@ interface TaggedUnionMatcherWidening<T extends object, K extends keyof T> {
): UnaryFn<T, PatternReturns<Handled> | R>;
}
type HandlerMap = Record<string | number, UnaryFn<never, unknown> | undefined>;
const dispatch =
(k: PropertyKey) =>
(handlers: HandlerMap, fallback?: UnaryFn<never, unknown>) =>