From 1ad19ba3088a7f7c994760f7c5ece5f099eeef5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Mon, 21 Sep 2026 10:33:36 +0000 Subject: [PATCH] :memo: Document matcher caveats in the README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit State the end-user limits once, in README § Caveats: a member colliding with its stringification, the unsupported symbol/bigint, and NaN/-0. Drop the duplicated known-issue prose from development/library.md and the source comment; link to the README instead. --- README.md | 14 +++++++++++++- development/library.md | 20 ++++---------------- src/primitive.ts | 7 ++----- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 5e86b41..f6b20e4 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ordinary objects whose `matches` method is a TypeScript type guard, so narrowing composes the way any other guard does. It is deliberately not a regex engine and not a macro: there is no transpiler and no DSL to learn, and the type-level contract is the feature — see [development/library.md](./development/library.md) -for the design decisions and the known limitations. +for the design decisions and [Caveats](#caveats) for the limits. ## Requirements @@ -23,6 +23,18 @@ for the design decisions and the known limitations. Yet to be implemented +## Caveats + +- **A value and its stringification collide.** Object keys stringify, so a + universe that mixes a member with the string it stringifies to — `1 | "1"`, + `true | "true"`, `null | "null"` — collapses to a single handler key and both + members are routed to it. Use one form or the other. +- **`symbol` and `bigint` are not supported.** A `symbol` brand is a + compile-time phantom with nothing to match at runtime, and a `bigint` is not a + valid property key; neither satisfies the matcher's universe constraint. +- **`NaN` and `-0` cannot be matched specifically.** They have no literal type, + so both stay part of `number`. + ## License MIT © 2025 tmu. See [LICENSE](./LICENSE). diff --git a/development/library.md b/development/library.md index 9f31e15..967feb9 100644 --- a/development/library.md +++ b/development/library.md @@ -97,7 +97,7 @@ Each factory is two overloads whose order is load-bearing: #### Decision (2026-09) The universe (`Matchable`) is `string | number | boolean | null | undefined`, -with `boolean` admitted as `true | false`. `symbol` and `bigint` are not. +with `boolean` admitted as `true | false`. `boolean`/`null`/`undefined` are not property keys, so handler-map keys are a projection (`PatternKey`: each member stringified) and `PatternParam` inverts @@ -109,18 +109,6 @@ offers `true`, `false`, `null`, `undefined` by name (verified over LSP). - Runtime dispatch is unchanged in effect: `handlers[true]` already coerces to `"true"`. `dispatch` wraps the index in `String()` only because TypeScript forbids indexing with `boolean`/`null`/`undefined` (`TS2538`). - -#### Rejected - -- **`symbol`.** A brand is a compile-time phantom — nothing to match at - runtime; the popup cannot offer symbol keys anyway. -- **`bigint`.** Not a valid property key; a stringified key (`"1"`) collides - with numeric `1`. -- **`NaN` / `-0`.** No literal type exists; they stay one `number`. - -#### Known issue - -- A universe mixing a member with its stringification (`1 | "1"`, - `true | "true"`, `null | "null"`) collapses to one handler key and routes - both members to it. Pre-existing for `1 | "1"`; now reachable for the new - members. Not guarded at the type level. +- `symbol`/`bigint`/`NaN`/`-0` are rejected, and a member colliding with its + stringification is unguarded: user-facing, stated once in + [README § Caveats](../README.md#caveats). diff --git a/src/primitive.ts b/src/primitive.ts index 78a1477..2c27b56 100644 --- a/src/primitive.ts +++ b/src/primitive.ts @@ -3,16 +3,13 @@ import type { Exact, ValueOf } from "type-fest"; type UnaryFn = (shape: T) => R; // The primitive universe a matcher can discriminate. `boolean` is admitted as -// the pair `true | false`; `symbol` is deliberately absent (a brand is a -// compile-time phantom, so there is nothing to match at runtime). +// the pair `true | false`; see README § Caveats for the unsupported members. type Matchable = string | number | boolean | null | undefined; // `boolean`, `null` and `undefined` cannot be property keys, so a mapped type // over the universe keys each non-key member by its stringification. `Param` // inverts that projection, so a handler callback still receives the *real* -// member (`true`, not `"true"`). A universe mixing a member with the string it -// stringifies to (e.g. `true | "true"`) collapses to one key and is not -// representable — see development/library.md. +// member (`true`, not `"true"`) — see README § Caveats for the limits. type PatternKey = T extends boolean ? T extends true ? "true"