♻️ Share matcher internals
This commit is contained in:
1 parent
6f4f509b69
commit
937d84ea48
4 files changed
+63
-26
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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>) =>
|
||||
|
||||
Reference in new issue
Block a user