✅ Spec mixed string and numeric union keys

This commit is contained in:
tmu committed 2026-09-16 20:01:12 +00:00
1 parent ed71929365
commit a0f0894339
1 file changed
+128
+128
View File
@@ -64,6 +64,40 @@ test("getPrimitiveUnionMatcherW dispatches on numeric literal keys", () => {
assert.equal(matcher(2), 20);
});
test("getPrimitiveUnionMatcherW dispatches on mixed string and numeric keys", () => {
// Arrange
const factory = getPrimitiveUnionMatcherW<"a" | "b" | 1 | 2>();
// Act
const matcher = factory({
a: (s) => {
expectTypeOf(s).toEqualTypeOf<"a">();
return "A" as const;
},
b: (s) => {
expectTypeOf(s).toEqualTypeOf<"b">();
return "B" as const;
},
1: (n) => {
expectTypeOf(n).toEqualTypeOf<1>();
return 10 as const;
},
2: (n) => {
expectTypeOf(n).toEqualTypeOf<2>();
return 20 as const;
},
});
// Assert
expectTypeOf(matcher).toEqualTypeOf<
(shape: "a" | "b" | 1 | 2) => "A" | "B" | 10 | 20
>();
assert.equal(matcher("a"), "A");
assert.equal(matcher("b"), "B");
assert.equal(matcher(1), 10);
assert.equal(matcher(2), 20);
});
// ============================================================================
// API: getPrimitiveUnionMatcher — ✔️ Exhaustive / ✔️ ReturnsStrict
// ============================================================================
@@ -113,6 +147,39 @@ test("getPrimitiveUnionMatcher handlers receive the matched literal", () => {
assert.equal(matcher("off"), "handler off");
});
test("getPrimitiveUnionMatcher infers one return type across mixed string and numeric keys", () => {
// Arrange
const factory = getPrimitiveUnionMatcher<"a" | "b" | 1 | 2>();
// Act
const matcher = factory({
a: (s): number => {
expectTypeOf(s).toEqualTypeOf<"a">();
return 1;
},
b: (s): 1 | 2 => {
expectTypeOf(s).toEqualTypeOf<"b">();
return 2;
},
1: (n): number => {
expectTypeOf(n).toEqualTypeOf<1>();
return 10;
},
2: (n): number => {
expectTypeOf(n).toEqualTypeOf<2>();
return 20;
},
});
// Assert
// ✔️ ReturnsStrict: R is the best common return type, not a widening union.
expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b" | 1 | 2) => number>();
assert.equal(matcher("a"), 1);
assert.equal(matcher("b"), 2);
assert.equal(matcher(1), 10);
assert.equal(matcher(2), 20);
});
// ============================================================================
// API: getPrimitiveUnionMatcherPartial — ❌ Exhaustive / ✔️ ReturnsStrict
// ============================================================================
@@ -169,6 +236,35 @@ test("getPrimitiveUnionMatcherPartial also accepts an exhaustive pattern", () =>
assert.equal(matcher("b"), "B");
});
test("getPrimitiveUnionMatcherPartial routes mixed string and numeric keys, gaps go to _", () => {
// Arrange
const factory = getPrimitiveUnionMatcherPartial<"a" | "b" | 1 | 2>();
// Act
const matcher = factory({
a: (s): string => {
expectTypeOf(s).toEqualTypeOf<"a">();
return "A";
},
1: (n): string => {
expectTypeOf(n).toEqualTypeOf<1>();
return "one";
},
_: (s): string => {
// The fallback sees the whole union, not a single literal.
expectTypeOf(s).toEqualTypeOf<"a" | "b" | 1 | 2>();
return "fallback";
},
});
// Assert
expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b" | 1 | 2) => string>();
assert.equal(matcher("a"), "A");
assert.equal(matcher("b"), "fallback");
assert.equal(matcher(1), "one");
assert.equal(matcher(2), "fallback");
});
// ============================================================================
// API: getPrimitiveUnionMatcherPartialW — ❌ Exhaustive / ❌ ReturnsStrict
// ============================================================================
@@ -205,6 +301,38 @@ test("getPrimitiveUnionMatcherPartialW allows gaps and widens to the union of ha
assert.equal(matcher("z"), "fallback");
});
test("getPrimitiveUnionMatcherPartialW widens mixed string and numeric key returns to their union", () => {
// Arrange
const factory = getPrimitiveUnionMatcherPartialW<"a" | "b" | 1 | 2>();
// Act
const matcher = factory({
a: (s) => {
expectTypeOf(s).toEqualTypeOf<"a">();
return "A" as const;
},
1: (n) => {
expectTypeOf(n).toEqualTypeOf<1>();
return 10 as const;
},
_: (s) => {
// The fallback sees the whole union, not a single literal.
expectTypeOf(s).toEqualTypeOf<"a" | "b" | 1 | 2>();
return "fallback" as const;
},
});
// Assert
// ❌ ReturnsStrict: mixed handler returns widen to their union.
expectTypeOf(matcher).toEqualTypeOf<
(shape: "a" | "b" | 1 | 2) => "A" | 10 | "fallback"
>();
assert.equal(matcher("a"), "A");
assert.equal(matcher("b"), "fallback");
assert.equal(matcher(1), 10);
assert.equal(matcher(2), "fallback");
});
test("getPrimitiveUnionMatcherPartialW also accepts an exhaustive pattern", () => {
// Arrange
const factory = getPrimitiveUnionMatcherPartialW<"x" | "y">();