✅ Assert the shape each handler receives

A handler's `expectTypeOf(shape)` only proved the type the compiler inferred;
nothing observed the argument `dispatch` passed. Passing `String(shape)` at the
call site therefore kept the whole suite green while breaking every key whose
property name differs from its value (`true`, `null`, `1`).

Pair each handler expectation with a runtime assertion: `assert.equal` where a
handler runs for one shape, the disjunction over the set a `_` fallback accepts
where it runs for several. Where the exact value matters the fallback returns
its shape verbatim and the call site asserts it; a widening pattern absorbs the
remainder type into the return union, so the claim stays strict-free. The
mutation above now fails 10 of the 33 tests.

Rule: CONTRIBUTING.md, rationale: development/testing.md § Handler arguments.
This commit is contained in:
tmu committed 2026-09-21 12:52:58 +00:00
1 parent aa6d71076c
commit 9e03c52473
3 files changed
+109 -11

No files matched your search

+71 -10
View File
@@ -23,10 +23,12 @@ test("getMatcher: exhaustive pattern infers one common return type", () => {
const matcher = factory({
a: (s) => {
expectTypeOf(s).toEqualTypeOf<"a">();
assert.equal(s, "a");
return 1;
},
b: (s) => {
expectTypeOf(s).toEqualTypeOf<"b">();
assert.equal(s, "b");
return 2;
},
});
@@ -48,10 +50,13 @@ test("getMatcher: dispatches on numeric literal keys", () => {
const matcher = factory({
1: (n) => {
expectTypeOf(n).toEqualTypeOf<1>();
// A numeric key must not reach its handler as the string `"1"`.
assert.equal(n, 1);
return n + 1;
},
2: (n) => {
expectTypeOf(n).toEqualTypeOf<2>();
assert.equal(n, 2);
return n * 10;
},
});
@@ -70,18 +75,22 @@ test("getMatcher: dispatches on mixed string and numeric keys", () => {
const matcher = factory({
a: (s) => {
expectTypeOf(s).toEqualTypeOf<"a">();
assert.equal(s, "a");
return 1;
},
b: (s) => {
expectTypeOf(s).toEqualTypeOf<"b">();
assert.equal(s, "b");
return 2;
},
1: (n) => {
expectTypeOf(n).toEqualTypeOf<1>();
assert.equal(n, 1);
return 10;
},
2: (n) => {
expectTypeOf(n).toEqualTypeOf<2>();
assert.equal(n, 2);
return 20;
},
});
@@ -102,10 +111,14 @@ test("getMatcher: dispatches on boolean literals", () => {
const matcher = factory({
true: (s) => {
expectTypeOf(s).toEqualTypeOf<true>();
// The `true` key is a property name; the handler must still be
// called with the boolean `true`, not the string `"true"`.
assert.equal(s, true);
return 1;
},
false: (s) => {
expectTypeOf(s).toEqualTypeOf<false>();
assert.equal(s, false);
return 2;
},
});
@@ -124,10 +137,12 @@ test("getMatcher: dispatches on null and undefined", () => {
const matcher = factory({
null: (s) => {
expectTypeOf(s).toEqualTypeOf<null>();
assert.equal(s, null);
return "null";
},
undefined: (s) => {
expectTypeOf(s).toEqualTypeOf<undefined>();
assert.equal(s, undefined);
return "undefined";
},
});
@@ -151,12 +166,14 @@ test("getMatcher: a `_` fallback receives the unhandled keys", () => {
{
a: (s) => {
expectTypeOf(s).toEqualTypeOf<"a">();
assert.equal(s, "a");
return 1 as const;
},
},
(s) => {
// The fallback sees only the keys `a` did not handle.
expectTypeOf(s).toEqualTypeOf<"b" | "c">();
assert.ok(s === "b" || s === "c");
return 2 as const;
},
);
@@ -179,15 +196,20 @@ test("getMatcher: a `_` fallback routes mixed string and numeric gaps", () => {
{
a: (s) => {
expectTypeOf(s).toEqualTypeOf<"a">();
assert.equal(s, "a");
return "A";
},
1: (n) => {
expectTypeOf(n).toEqualTypeOf<1>();
assert.equal(n, 1);
return "one";
},
},
(s) => {
expectTypeOf(s).toEqualTypeOf<"b" | 2>();
// The gap mixes a string and a number, so the number must reach the
// fallback as `2`, never as its key text `"2"`.
assert.ok(s === "b" || s === 2);
return "fallback";
},
);
@@ -209,10 +231,12 @@ test("getMatcher: a `_` fallback receives unhandled boolean and nullish keys", (
{
a: (s) => {
expectTypeOf(s).toEqualTypeOf<"a">();
assert.equal(s, "a");
return 1 as const;
},
null: (s) => {
expectTypeOf(s).toEqualTypeOf<null>();
assert.equal(s, null);
return 2 as const;
},
},
@@ -220,6 +244,7 @@ test("getMatcher: a `_` fallback receives unhandled boolean and nullish keys", (
// `true` and `false` are keyed as `"true"`/`"false"` but the
// fallback still sees them as booleans.
expectTypeOf(s).toEqualTypeOf<true | false | undefined>();
assert.ok(s === true || s === false || s === undefined);
return 3 as const;
},
);
@@ -247,10 +272,12 @@ test("getMatcherW: exhaustive pattern widens to the union of handler returns", (
const matcher = factory({
x: (s) => {
expectTypeOf(s).toEqualTypeOf<"x">();
assert.equal(s, "x");
return 1 as const;
},
y: (s) => {
expectTypeOf(s).toEqualTypeOf<"y">();
assert.equal(s, "y");
return "two" as const;
},
});
@@ -272,18 +299,22 @@ test("getMatcherW: dispatches on mixed string and numeric keys", () => {
const matcher = factory({
a: (s) => {
expectTypeOf(s).toEqualTypeOf<"a">();
assert.equal(s, "a");
return "A" as const;
},
b: (s) => {
expectTypeOf(s).toEqualTypeOf<"b">();
assert.equal(s, "b");
return "B" as const;
},
1: (n) => {
expectTypeOf(n).toEqualTypeOf<1>();
assert.equal(n, 1);
return 10 as const;
},
2: (n) => {
expectTypeOf(n).toEqualTypeOf<2>();
assert.equal(n, 2);
return 20 as const;
},
});
@@ -306,18 +337,22 @@ test("getMatcherW: exhaustive boolean and nullish widen to the union", () => {
const matcher = factory({
true: (s) => {
expectTypeOf(s).toEqualTypeOf<true>();
assert.equal(s, true);
return "yes" as const;
},
false: (s) => {
expectTypeOf(s).toEqualTypeOf<false>();
assert.equal(s, false);
return "no" as const;
},
null: (s) => {
expectTypeOf(s).toEqualTypeOf<null>();
assert.equal(s, null);
return 0 as const;
},
undefined: (s) => {
expectTypeOf(s).toEqualTypeOf<undefined>();
assert.equal(s, undefined);
return 1 as const;
},
});
@@ -345,25 +380,28 @@ test("getMatcherW: a `_` fallback widens gaps into the union", () => {
{
x: (s) => {
expectTypeOf(s).toEqualTypeOf<"x">();
assert.equal(s, "x");
return 1 as const;
},
},
(s) => {
// The fallback sees only the keys `x` did not handle.
expectTypeOf(s).toEqualTypeOf<"y" | "z">();
return "fallback" as const;
// Returning the shape verbatim lets the `Assert` block check the
// exact value dispatch passed.
return s;
},
);
// Assert
expectTypeOf(matcher).toEqualTypeOf<
(shape: "x" | "y" | "z") => 1 | "fallback"
(shape: "x" | "y" | "z") => 1 | "y" | "z"
>();
// ❌ a value outside T must not be accepted by the matcher
expectTypeOf<"w">().not.toExtend<Parameters<typeof matcher>[0]>();
assert.equal(matcher("x"), 1);
assert.equal(matcher("y"), "fallback");
assert.equal(matcher("z"), "fallback");
assert.equal(matcher("y"), "y");
assert.equal(matcher("z"), "z");
});
test("getMatcherW: a `_` fallback widens mixed string and numeric returns", () => {
@@ -375,28 +413,34 @@ test("getMatcherW: a `_` fallback widens mixed string and numeric returns", () =
{
a: (s) => {
expectTypeOf(s).toEqualTypeOf<"a">();
assert.equal(s, "a");
return "A" as const;
},
1: (n) => {
expectTypeOf(n).toEqualTypeOf<1>();
assert.equal(n, 1);
return 10 as const;
},
},
(s) => {
expectTypeOf(s).toEqualTypeOf<"b" | 2>();
return "fallback" as const;
// The number must arrive as `2`, not as its key text `"2"`.
assert.ok(s === "b" || s === 2);
// Returning the shape verbatim lets the `Assert` block check the
// exact value dispatch passed.
return s;
},
);
// Assert
// ❌ ReturnsStrict: mixed handler returns widen to their union.
expectTypeOf(matcher).toEqualTypeOf<
(shape: "a" | "b" | 1 | 2) => "A" | 10 | "fallback"
(shape: "a" | "b" | 1 | 2) => "A" | 10 | "b" | 2
>();
assert.equal(matcher("a"), "A");
assert.equal(matcher("b"), "fallback");
assert.equal(matcher("b"), "b");
assert.equal(matcher(1), 10);
assert.equal(matcher(2), "fallback");
assert.equal(matcher(2), 2);
});
// ============================================================================
@@ -455,6 +499,10 @@ test("getMatcher: an open universe keeps the fallback's remainder open", () => {
// The map's literal keys do not close an open universe, so the
// remainder stays `string` and the fallback is not redundant.
expectTypeOf(s).toEqualTypeOf<string>();
// A strict pattern fixes one `R` for every handler, so this fallback
// cannot return its shape; `typeof` is the strongest claim the value
// makes on its own — any string passes, including `""`.
assert.equal(typeof s, "string");
return 2 as const;
});
@@ -488,7 +536,13 @@ test("getMatcherW factory rejects patterns outside its contract", () => {
test("getMatcher: 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, () => number> = { a: () => 1 };
const handlers: Record<string, (shape: unknown) => number> = {
a: (shape) => {
// The raw shape reaches the handler, not the property key's text.
assert.equal(shape, "a");
return 1;
},
};
const matcher = getMatcher<string>()(handlers);
// Act / Assert
@@ -499,7 +553,14 @@ test("getMatcher: an unhandled shape throws without a fallback", () => {
test("getMatcher: 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, () => number> = { true: () => 1 };
const handlers: Record<string, (shape: unknown) => number> = {
true: (shape) => {
// `true` indexes the map as the property `"true"`, but the handler
// is still called with the boolean.
assert.equal(shape, true);
return 1;
},
};
const matcher = getMatcher<string | boolean>()(handlers);
// Act / Assert