From 6228b4922d3f36ecd45f1a6ee71c85430b560906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 22 Sep 2026 10:54:27 +0000 Subject: [PATCH] :recycle: Share the Matchable universe between both matchers --- development/library.md | 14 +++++++++----- src/matcher-shared.ts | 6 ++++++ src/primitive-union.ts | 5 +---- src/tagged-union.ts | 16 ++++++++-------- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/development/library.md b/development/library.md index 49ec1db..9196b90 100644 --- a/development/library.md +++ b/development/library.md @@ -97,9 +97,9 @@ Each factory is two overloads whose order is load-bearing: #### Decision (2026-09) -`src/matcher-shared.ts` holds the six universe-agnostic pieces both matchers -use: `UnaryFn`, `PatternReturns`, `RedundantFallback`, `HandlerMap`, and the -`PatternKey` / `PatternParam` key projection. +`src/matcher-shared.ts` holds the seven universe-agnostic pieces both matchers +use: `UnaryFn`, `PatternReturns`, `RedundantFallback`, `HandlerMap`, the shared +`Matchable` universe, and the `PatternKey` / `PatternParam` key projection. #### Why @@ -107,6 +107,10 @@ use: `UnaryFn`, `PatternReturns`, `RedundantFallback`, `HandlerMap`, and the keeps the two matchers' message from drifting; the other pieces appear verbatim in both public signatures or are the same projection over each matcher's universe. +- **`Matchable` is one definition, not two.** The primitive-union matcher's + universe and the tagged-union matcher's allowed `Tag` values are the same set, + so aliasing them keeps the two matchers from drifting apart on what they + accept (`symbol`/`bigint` rejected once). #### Rejected @@ -114,8 +118,8 @@ use: `UnaryFn`, `PatternReturns`, `RedundantFallback`, `HandlerMap`, and the `Fallback` and `MustBePartial`.** Each is built from its own universe (`Tags`/`MapTaggedUnion` vs the primitive values); abstracting over the F-bounded `Handled` constraint that makes the remainder work risks the - contextual typing it exists to preserve. The `PatternKey` / `PatternParam` - projection is the one piece both universes genuinely share. + contextual typing it exists to preserve. `Matchable` and the `PatternKey` / + `PatternParam` projection are the pieces both universes genuinely share. ## Tagged-union matcher diff --git a/src/matcher-shared.ts b/src/matcher-shared.ts index 45dc574..9c302fc 100644 --- a/src/matcher-shared.ts +++ b/src/matcher-shared.ts @@ -8,6 +8,12 @@ import type { ValueOf } from "type-fest"; // A handler: one universe member in, one return value out. export type UnaryFn = (shape: T) => R; +// The primitive universe a matcher can discriminate. The primitive-union matcher +// uses it directly; the tagged-union matcher uses it as the set of allowed +// discriminant (`Tag`) values. `boolean` is admitted as the pair `true | false`; +// see README § Caveats for the unsupported members. +export type Matchable = string | number | boolean | null | undefined; + // `boolean`, `null` and `undefined` cannot be property keys, so a mapped type // over a universe that includes one keys each such member by its // stringification. `PatternParam` inverts that projection, so a handler callback diff --git a/src/primitive-union.ts b/src/primitive-union.ts index 76b58d8..bebc727 100644 --- a/src/primitive-union.ts +++ b/src/primitive-union.ts @@ -2,6 +2,7 @@ import type { Exact } from "type-fest"; import type { HandlerMap, + Matchable, PatternKey, PatternParam, PatternReturns, @@ -9,10 +10,6 @@ import type { 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. -type Matchable = string | number | boolean | null | undefined; - type Handlers = { [K in PatternKey]: UnaryFn, R>; }; diff --git a/src/tagged-union.ts b/src/tagged-union.ts index d092e65..26b0a00 100644 --- a/src/tagged-union.ts +++ b/src/tagged-union.ts @@ -2,6 +2,7 @@ import type { Exact, UnknownRecord } from "type-fest"; import type { HandlerMap, + Matchable, PatternKey, PatternParam, PatternReturns, @@ -9,22 +10,21 @@ import type { UnaryFn, } from "./matcher-shared.ts"; -// A tagged union is discriminated by one property whose values are the tags. -// `string` and `number` tags key a handler map directly; `boolean`, `null` and -// `undefined` are admitted too but are not property keys, so they go through the -// `PatternKey` projection. `symbol` has no literal syntax to write a handler -// under, and `bigint` is not a property key. -type Tag = string | number | boolean | null | undefined; +// A tagged union is discriminated by one property whose values are the tags (a +// `Matchable`). `string` and `number` tags key a handler map directly; +// `boolean`, `null` and `undefined` are admitted too but are not property keys, +// so they go through the `PatternKey` projection. `symbol` has no literal syntax +// 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. -type Tags = Extract; +type Tags = Extract; // The keys of `T` that can act as a discriminant. `getTaggedUnionMatcher()` // accepts only these, so the factory rejects a key whose values are not tags. type Discriminated = { - [K in keyof T]: T[K] extends Tag ? K : never; + [K in keyof T]: T[K] extends Matchable ? K : never; }[keyof T]; // The member(s) of `T` tagged `V`. `Extract` distributes over the union, so a