♻️ Rename primitive to primitive-union

`getMatcher` / `getMatcherW` become `getPrimitiveUnionMatcher` /
`getPrimitiveUnionMatcherW`; `src/primitive.ts` and its test move to
`src/primitive-union.*`. The `Union` suffix mirrors `getTaggedUnionMatcher`.
This commit is contained in:
tmu committed 2026-09-22 09:46:59 +00:00
1 parent 0727117afb
commit 02f736507c
10 files changed
+97 -83

No files matched your search

+4 -1
View File
@@ -1,4 +1,7 @@
export { getMatcher, getMatcherW } from "./primitive.ts";
export {
getPrimitiveUnionMatcher,
getPrimitiveUnionMatcherW,
} from "./primitive-union.ts";
export {
getTaggedUnionMatcher,
getTaggedUnionMatcherW,
+1 -1
View File
@@ -1,6 +1,6 @@
import type { ValueOf } from "type-fest";
// The primitive and tagged-union matchers differ in their universe, but the
// The primitive-union 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.
@@ -9,15 +9,18 @@ import {
LspSession,
} from "#test-utils/lsp-completion.ts";
import { getMatcher, getMatcherW } from "./primitive.ts";
import {
getPrimitiveUnionMatcher,
getPrimitiveUnionMatcherW,
} from "./primitive-union.ts";
// ============================================================================
// API: getMatcher — ✔️ Exhaustive / ✔️ ReturnsStrict
// API: getPrimitiveUnionMatcher — ✔️ Exhaustive / ✔️ ReturnsStrict
// ============================================================================
test("getMatcher: exhaustive pattern infers one common return type", () => {
test("getPrimitiveUnionMatcher: exhaustive pattern infers one common return type", () => {
// Arrange
const factory = getMatcher<"a" | "b">();
const factory = getPrimitiveUnionMatcher<"a" | "b">();
// Act
const matcher = factory({
@@ -42,9 +45,9 @@ test("getMatcher: exhaustive pattern infers one common return type", () => {
assert.equal(matcher("b"), 2);
});
test("getMatcher: dispatches on numeric literal keys", () => {
test("getPrimitiveUnionMatcher: dispatches on numeric literal keys", () => {
// Arrange
const factory = getMatcher<1 | 2>();
const factory = getPrimitiveUnionMatcher<1 | 2>();
// Act
const matcher = factory({
@@ -67,9 +70,9 @@ test("getMatcher: dispatches on numeric literal keys", () => {
assert.equal(matcher(2), 20);
});
test("getMatcher: dispatches on mixed string and numeric keys", () => {
test("getPrimitiveUnionMatcher: dispatches on mixed string and numeric keys", () => {
// Arrange
const factory = getMatcher<"a" | "b" | 1 | 2>();
const factory = getPrimitiveUnionMatcher<"a" | "b" | 1 | 2>();
// Act
const matcher = factory({
@@ -103,9 +106,9 @@ test("getMatcher: dispatches on mixed string and numeric keys", () => {
assert.equal(matcher(2), 20);
});
test("getMatcher: dispatches on boolean literals", () => {
test("getPrimitiveUnionMatcher: dispatches on boolean literals", () => {
// Arrange
const factory = getMatcher<boolean>();
const factory = getPrimitiveUnionMatcher<boolean>();
// Act
const matcher = factory({
@@ -129,9 +132,9 @@ test("getMatcher: dispatches on boolean literals", () => {
assert.equal(matcher(false), 2);
});
test("getMatcher: dispatches on null and undefined", () => {
test("getPrimitiveUnionMatcher: dispatches on null and undefined", () => {
// Arrange
const factory = getMatcher<null | undefined>();
const factory = getPrimitiveUnionMatcher<null | undefined>();
// Act
const matcher = factory({
@@ -154,12 +157,12 @@ test("getMatcher: dispatches on null and undefined", () => {
});
// ============================================================================
// API: getMatcher — ❌ Exhaustive (fallback) / ✔️ ReturnsStrict
// API: getPrimitiveUnionMatcher — ❌ Exhaustive (fallback) / ✔️ ReturnsStrict
// ============================================================================
test("getMatcher: a `_` fallback receives the unhandled keys", () => {
test("getPrimitiveUnionMatcher: a `_` fallback receives the unhandled keys", () => {
// Arrange
const factory = getMatcher<"a" | "b" | "c">();
const factory = getPrimitiveUnionMatcher<"a" | "b" | "c">();
// Act
const matcher = factory(
@@ -187,9 +190,9 @@ test("getMatcher: a `_` fallback receives the unhandled keys", () => {
assert.equal(matcher("c"), 2);
});
test("getMatcher: a `_` fallback routes mixed string and numeric gaps", () => {
test("getPrimitiveUnionMatcher: a `_` fallback routes mixed string and numeric gaps", () => {
// Arrange
const factory = getMatcher<"a" | "b" | 1 | 2>();
const factory = getPrimitiveUnionMatcher<"a" | "b" | 1 | 2>();
// Act
const matcher = factory(
@@ -222,9 +225,11 @@ test("getMatcher: a `_` fallback routes mixed string and numeric gaps", () => {
assert.equal(matcher(2), "fallback");
});
test("getMatcher: a `_` fallback receives unhandled boolean and nullish keys", () => {
test("getPrimitiveUnionMatcher: a `_` fallback receives unhandled boolean and nullish keys", () => {
// Arrange
const factory = getMatcher<"a" | true | false | null | undefined>();
const factory = getPrimitiveUnionMatcher<
"a" | true | false | null | undefined
>();
// Act
const matcher = factory(
@@ -261,12 +266,12 @@ test("getMatcher: a `_` fallback receives unhandled boolean and nullish keys", (
});
// ============================================================================
// API: getMatcherW — ✔️ Exhaustive / ❌ ReturnsStrict
// API: getPrimitiveUnionMatcherW — ✔️ Exhaustive / ❌ ReturnsStrict
// ============================================================================
test("getMatcherW: exhaustive pattern widens to the union of handler returns", () => {
test("getPrimitiveUnionMatcherW: exhaustive pattern widens to the union of handler returns", () => {
// Arrange
const factory = getMatcherW<"x" | "y">();
const factory = getPrimitiveUnionMatcherW<"x" | "y">();
// Act
const matcher = factory({
@@ -291,9 +296,9 @@ test("getMatcherW: exhaustive pattern widens to the union of handler returns", (
assert.equal(matcher("y"), "two");
});
test("getMatcherW: dispatches on mixed string and numeric keys", () => {
test("getPrimitiveUnionMatcherW: dispatches on mixed string and numeric keys", () => {
// Arrange
const factory = getMatcherW<"a" | "b" | 1 | 2>();
const factory = getPrimitiveUnionMatcherW<"a" | "b" | 1 | 2>();
// Act
const matcher = factory({
@@ -329,9 +334,9 @@ test("getMatcherW: dispatches on mixed string and numeric keys", () => {
assert.equal(matcher(2), 20);
});
test("getMatcherW: exhaustive boolean and nullish widen to the union", () => {
test("getPrimitiveUnionMatcherW: exhaustive boolean and nullish widen to the union", () => {
// Arrange
const factory = getMatcherW<boolean | null | undefined>();
const factory = getPrimitiveUnionMatcherW<boolean | null | undefined>();
// Act
const matcher = factory({
@@ -368,12 +373,12 @@ test("getMatcherW: exhaustive boolean and nullish widen to the union", () => {
});
// ============================================================================
// API: getMatcherW — ❌ Exhaustive (fallback) / ❌ ReturnsStrict
// API: getPrimitiveUnionMatcherW — ❌ Exhaustive (fallback) / ❌ ReturnsStrict
// ============================================================================
test("getMatcherW: a `_` fallback widens gaps into the union", () => {
test("getPrimitiveUnionMatcherW: a `_` fallback widens gaps into the union", () => {
// Arrange
const factory = getMatcherW<"x" | "y" | "z">();
const factory = getPrimitiveUnionMatcherW<"x" | "y" | "z">();
// Act
const matcher = factory(
@@ -404,9 +409,9 @@ test("getMatcherW: a `_` fallback widens gaps into the union", () => {
assert.equal(matcher("z"), "z");
});
test("getMatcherW: a `_` fallback widens mixed string and numeric returns", () => {
test("getPrimitiveUnionMatcherW: a `_` fallback widens mixed string and numeric returns", () => {
// Arrange
const factory = getMatcherW<"a" | "b" | 1 | 2>();
const factory = getPrimitiveUnionMatcherW<"a" | "b" | 1 | 2>();
// Act
const matcher = factory(
@@ -447,9 +452,9 @@ test("getMatcherW: a `_` fallback widens mixed string and numeric returns", () =
// Factory contracts — calls that must not compile
// ============================================================================
test("getMatcher factory rejects patterns outside its contract", () => {
test("getPrimitiveUnionMatcher factory rejects patterns outside its contract", () => {
// Arrange
const factory = getMatcher<"a" | "b">();
const factory = getPrimitiveUnionMatcher<"a" | "b">();
// Act / Assert — the calls below must not compile.
// `Parameters<typeof factory>[0]` resolves only the *last* overload, so it
@@ -469,9 +474,9 @@ test("getMatcher factory rejects patterns outside its contract", () => {
factory({ a: () => 1, b: () => 2 }, () => 0);
});
test("getMatcher factory rejects non-exhaustive boolean and nullish maps", () => {
test("getPrimitiveUnionMatcher factory rejects non-exhaustive boolean and nullish maps", () => {
// Arrange
const factory = getMatcher<boolean | null | undefined>();
const factory = getPrimitiveUnionMatcher<boolean | null | undefined>();
// Act / Assert — the calls below must not compile
// @ts-expect-error only `true` is handled; `false`, `null`, `undefined` are not
@@ -490,9 +495,9 @@ test("getMatcher factory rejects non-exhaustive boolean and nullish maps", () =>
);
});
test("getMatcher: an open universe keeps the fallback's remainder open", () => {
test("getPrimitiveUnionMatcher: an open universe keeps the fallback's remainder open", () => {
// Arrange
const factory = getMatcher<string>();
const factory = getPrimitiveUnionMatcher<string>();
// Act
const matcher = factory({ a: () => 1 }, (s) => {
@@ -512,9 +517,9 @@ test("getMatcher: an open universe keeps the fallback's remainder open", () => {
assert.equal(matcher("b"), 2);
});
test("getMatcherW factory rejects patterns outside its contract", () => {
test("getPrimitiveUnionMatcherW factory rejects patterns outside its contract", () => {
// Arrange
const factory = getMatcherW<"x" | "y">();
const factory = getPrimitiveUnionMatcherW<"x" | "y">();
// Act / Assert — the calls below must not compile
factory({
@@ -533,7 +538,7 @@ test("getMatcherW factory rejects patterns outside its contract", () => {
// Dispatch — runtime behavior
// ============================================================================
test("getMatcher: an unhandled shape throws without a fallback", () => {
test("getPrimitiveUnionMatcher: an unhandled shape throws without a fallback", () => {
// Arrange — an open universe types its handler map as an index signature,
// so the type system cannot prove the runtime map is exhaustive.
const handlers: Record<string, (shape: unknown) => number> = {
@@ -543,14 +548,14 @@ test("getMatcher: an unhandled shape throws without a fallback", () => {
return 1;
},
};
const matcher = getMatcher<string>()(handlers);
const matcher = getPrimitiveUnionMatcher<string>()(handlers);
// Act / Assert
assert.equal(matcher("a"), 1);
assert.throws(() => matcher("b"), /Unhandled shape: b/);
});
test("getMatcher: an unhandled boolean shape throws without a fallback", () => {
test("getPrimitiveUnionMatcher: an unhandled boolean shape throws without a fallback", () => {
// Arrange — an `string | boolean` universe widens its handler map to an
// index signature, so the runtime map's exhaustiveness is not provable.
const handlers: Record<string, (shape: unknown) => number> = {
@@ -561,7 +566,7 @@ test("getMatcher: an unhandled boolean shape throws without a fallback", () => {
return 1;
},
};
const matcher = getMatcher<string | boolean>()(handlers);
const matcher = getPrimitiveUnionMatcher<string | boolean>()(handlers);
// Act / Assert
assert.equal(matcher(true), 1);
@@ -584,7 +589,7 @@ const UNIVERSE = `"a" | "b" | "c"`;
// documents, project membership), so they pass in any order.
interface LabelsProbe {
readonly name: string;
readonly factory: "getMatcher" | "getMatcherW";
readonly factory: "getPrimitiveUnionMatcher" | "getPrimitiveUnionMatcherW";
readonly body: string;
readonly universe?: string;
readonly tail?: string;
@@ -616,12 +621,12 @@ const labelsFor = ({
test("autocomplete: an exhaustive pattern requires the universe", () => {
// Arrange
const name = "getMatcher_fresh";
const name = "getPrimitiveUnionMatcher_fresh";
// Act
const labels = labelsFor({
name,
factory: "getMatcher",
factory: "getPrimitiveUnionMatcher",
body: " /*COMPLETE*/",
});
@@ -633,12 +638,12 @@ test("autocomplete: an exhaustive pattern requires the universe", () => {
test("autocomplete: handled keys drop out of the popup", () => {
// Arrange
const name = "getMatcher_after_key";
const name = "getPrimitiveUnionMatcher_after_key";
// Act
const labels = labelsFor({
name,
factory: "getMatcher",
factory: "getPrimitiveUnionMatcher",
body: " a: () => 1,\n /*COMPLETE*/",
});
@@ -650,12 +655,12 @@ test("autocomplete: handled keys drop out of the popup", () => {
test("autocomplete: a fallback makes the remaining keys optional", () => {
// Arrange
const name = "getMatcher_with_fallback";
const name = "getPrimitiveUnionMatcher_with_fallback";
// Act
const labels = labelsFor({
name,
factory: "getMatcher",
factory: "getPrimitiveUnionMatcher",
body: " /*COMPLETE*/",
tail: ", () => 0",
});
@@ -668,12 +673,12 @@ test("autocomplete: a fallback makes the remaining keys optional", () => {
test("autocomplete: with a fallback, handled keys stay optional", () => {
// Arrange
const name = "getMatcher_with_fallback_after_key";
const name = "getPrimitiveUnionMatcher_with_fallback_after_key";
// Act
const labels = labelsFor({
name,
factory: "getMatcher",
factory: "getPrimitiveUnionMatcher",
body: " a: () => 1,\n /*COMPLETE*/",
tail: ", () => 0",
});
@@ -684,14 +689,14 @@ test("autocomplete: with a fallback, handled keys stay optional", () => {
});
});
test("autocomplete: `getMatcherW` offers the same popup as `getMatcher`", () => {
test("autocomplete: `getPrimitiveUnionMatcherW` offers the same popup as `getPrimitiveUnionMatcher`", () => {
// Arrange
const name = "getMatcherW_fresh";
const name = "getPrimitiveUnionMatcherW_fresh";
// Act
const labels = labelsFor({
name,
factory: "getMatcherW",
factory: "getPrimitiveUnionMatcherW",
body: " /*COMPLETE*/",
});
@@ -701,14 +706,14 @@ test("autocomplete: `getMatcherW` offers the same popup as `getMatcher`", () =>
});
});
test("autocomplete: `getMatcherW` also offers optional keys with a fallback", () => {
test("autocomplete: `getPrimitiveUnionMatcherW` also offers optional keys with a fallback", () => {
// Arrange
const name = "getMatcherW_with_fallback";
const name = "getPrimitiveUnionMatcherW_with_fallback";
// Act
const labels = labelsFor({
name,
factory: "getMatcherW",
factory: "getPrimitiveUnionMatcherW",
body: " /*COMPLETE*/",
tail: ", () => 0",
});
@@ -721,12 +726,12 @@ test("autocomplete: `getMatcherW` also offers optional keys with a fallback", ()
test("autocomplete: boolean and nullish keys are offered by name", () => {
// Arrange
const name = "getMatcher_boolean_nullish";
const name = "getPrimitiveUnionMatcher_boolean_nullish";
// Act
const labels = labelsFor({
name,
factory: "getMatcher",
factory: "getPrimitiveUnionMatcher",
universe: "boolean | null | undefined",
body: " /*COMPLETE*/",
});
@@ -739,12 +744,12 @@ test("autocomplete: boolean and nullish keys are offered by name", () => {
test("autocomplete: a handled boolean literal drops out of the popup", () => {
// Arrange
const name = "getMatcher_boolean_after_key";
const name = "getPrimitiveUnionMatcher_boolean_after_key";
// Act
const labels = labelsFor({
name,
factory: "getMatcher",
factory: "getPrimitiveUnionMatcher",
universe: "boolean | null | undefined",
body: " true: () => 1,\n /*COMPLETE*/",
});
+6 -3
View File
@@ -123,6 +123,9 @@ const dispatch =
shape as never,
);
export const getMatcher = <T extends Matchable>(): MatcherStrict<T> => dispatch;
export const getMatcherW = <T extends Matchable>(): MatcherWidening<T> =>
dispatch;
export const getPrimitiveUnionMatcher = <
T extends Matchable,
>(): MatcherStrict<T> => dispatch;
export const getPrimitiveUnionMatcherW = <
T extends Matchable,
>(): MatcherWidening<T> => dispatch;
+2 -2
View File
@@ -36,7 +36,7 @@ type Handlers<T extends object, K extends keyof T, R> = {
// 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
// the primitive matcher's `Exclude<T, PatternParam<keyof Handled>>`.
// the primitive-union matcher's `Exclude<T, PatternParam<keyof Handled>>`.
type HandledMembers<T extends object, K extends keyof T, Handled> = {
[V in Tags<T, K>]: V extends keyof Handled
? MapTaggedUnion<T, K>[V]
@@ -52,7 +52,7 @@ type Fallback<T extends object, K extends keyof T, Handled, R> = UnaryFn<
// A fallback is redundant once the handler map covers every tag of `T`. Folded
// 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
// inference; see the primitive-union matcher for why a conditional in the fallback's
// parameter is evaluated too early.
type MustBePartial<T extends object, K extends keyof T, Handled> =
Tags<T, K> extends keyof Handled ? RedundantFallback : unknown;