diff --git a/src/primitive.test.ts b/src/primitive.test.ts index 1182311..ee9f57e 100644 --- a/src/primitive.test.ts +++ b/src/primitive.test.ts @@ -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">();