import { strict as assert } from "node:assert"; import path from "node:path"; import { test } from "node:test"; import { expectTypeOf } from "expect-type"; import { type CompletionTarget, LspSession, } from "#test-utils/lsp-completion.ts"; import { getTaggedUnionMatcher, getTaggedUnionMatcherW, } from "./tagged-union.ts"; interface Circle { readonly kind: "circle"; readonly radius: number; } interface Square { readonly kind: "square"; readonly side: number; } interface Triangle { readonly kind: "triangle"; readonly base: number; readonly height: number; } type Shape = Circle | Square | Triangle; // ============================================================================ // API: getTaggedUnionMatcher — ✔️ Exhaustive / ✔️ ReturnsStrict // ============================================================================ test("getTaggedUnionMatcher: exhaustive pattern infers one common return type", () => { // Arrange const factory = getTaggedUnionMatcher()("kind"); // Act const area = factory({ circle: (s) => { // Each handler receives the member its tag selects, not the union. expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "circle"); return Math.PI * s.radius ** 2; }, square: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "square"); return s.side ** 2; }, triangle: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "triangle"); return (s.base * s.height) / 2; }, }); // Assert // ✔️ ReturnsStrict: R is the best common return type, not a widening union. expectTypeOf(area).toEqualTypeOf<(shape: Shape) => number>(); assert.equal(area({ kind: "square", side: 2 }), 4); assert.equal(area({ kind: "triangle", base: 2, height: 3 }), 3); }); test("getTaggedUnionMatcher: dispatches on numeric tags", () => { // Arrange type Version = | { readonly kind: 1; readonly a: number } | { readonly kind: 2; readonly b: number; }; const factory = getTaggedUnionMatcher()("kind"); // Act const pick = factory({ 1: (v) => { expectTypeOf(v).toEqualTypeOf<{ readonly kind: 1; readonly a: number; }>(); assert.equal(v.kind, 1); return v.a; }, 2: (v) => { expectTypeOf(v).toEqualTypeOf<{ readonly kind: 2; readonly b: number; }>(); assert.equal(v.kind, 2); return v.b; }, }); // Assert expectTypeOf(pick).toEqualTypeOf<(shape: Version) => number>(); assert.equal(pick({ kind: 1, a: 10 }), 10); assert.equal(pick({ kind: 2, b: 20 }), 20); }); test("getTaggedUnionMatcher: dispatches on boolean tags", () => { // Arrange — a boolean discriminant is not a property key, so the handler // map is keyed `true`/`false` and the callback still receives the boolean. interface Ok { readonly status: true; readonly value: number; } interface Err { readonly status: false; readonly message: string; } type Result = Ok | Err; const factory = getTaggedUnionMatcher()("status"); // Act const pick = factory({ true: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.status, true); return s.value; }, false: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.status, false); return s.message.length; }, }); // Assert expectTypeOf(pick).toEqualTypeOf<(shape: Result) => number>(); assert.equal(pick({ status: true, value: 3 }), 3); assert.equal(pick({ status: false, message: "hi" }), 2); }); test("getTaggedUnionMatcher: dispatches on null and undefined tags", () => { // Arrange type Maybe = | { readonly kind: null; readonly text: string } | { readonly kind: undefined; readonly code: number }; const factory = getTaggedUnionMatcher()("kind"); // Act const pick = factory({ null: (s) => { expectTypeOf(s).toEqualTypeOf<{ readonly kind: null; readonly text: string; }>(); assert.equal(s.kind, null); return s.text.length; }, undefined: (s) => { expectTypeOf(s).toEqualTypeOf<{ readonly kind: undefined; readonly code: number; }>(); assert.equal(s.kind, undefined); return s.code; }, }); // Assert expectTypeOf(pick).toEqualTypeOf<(shape: Maybe) => number>(); assert.equal(pick({ kind: null, text: "hi" }), 2); assert.equal(pick({ kind: undefined, code: 7 }), 7); }); test("getTaggedUnionMatcher: a fallback receives unhandled boolean and nullish members", () => { // Arrange type Mixed = | { readonly kind: "a"; readonly a: number } | { readonly kind: true; readonly b: number } | { readonly kind: false; readonly c: number } | { readonly kind: null; readonly d: number }; const factory = getTaggedUnionMatcher()("kind"); // Act const pick = factory({ a: () => 1 as const }, (s) => { // The fallback sees the real members, not the projected keys. expectTypeOf(s).toEqualTypeOf< | { readonly kind: true; readonly b: number } | { readonly kind: false; readonly c: number } | { readonly kind: null; readonly d: number } >(); // `true`/`false`/`null` are keyed as `"true"`/`"false"`/`"null"`, // but the fallback still receives the boolean/null, not the string. assert.ok(s.kind === true || s.kind === false || s.kind === null); return 2 as const; }); // Assert expectTypeOf(pick).toEqualTypeOf<(shape: Mixed) => 1 | 2>(); assert.equal(pick({ kind: "a", a: 1 }), 1); assert.equal(pick({ kind: true, b: 1 }), 2); assert.equal(pick({ kind: false, c: 1 }), 2); assert.equal(pick({ kind: null, d: 1 }), 2); }); test("getTaggedUnionMatcherW: boolean and nullish tags widen to the union", () => { // Arrange type Maybe = | { readonly kind: true; readonly a: number } | { readonly kind: false; readonly b: number } | { readonly kind: null; readonly c: number }; const factory = getTaggedUnionMatcherW()("kind"); // Act const matcher = factory({ true: (s) => { expectTypeOf(s).toEqualTypeOf<{ readonly kind: true; readonly a: number; }>(); assert.equal(s.kind, true); return "yes" as const; }, false: (s) => { expectTypeOf(s).toEqualTypeOf<{ readonly kind: false; readonly b: number; }>(); assert.equal(s.kind, false); return 0 as const; }, null: (s) => { expectTypeOf(s).toEqualTypeOf<{ readonly kind: null; readonly c: number; }>(); assert.equal(s.kind, null); return null; }, }); // Assert expectTypeOf(matcher).toEqualTypeOf<(shape: Maybe) => "yes" | 0 | null>(); assert.equal(matcher({ kind: true, a: 1 }), "yes"); assert.equal(matcher({ kind: false, b: 1 }), 0); assert.equal(matcher({ kind: null, c: 1 }), null); }); // ============================================================================ // API: getTaggedUnionMatcher — ❌ Exhaustive (fallback) / ✔️ ReturnsStrict // ============================================================================ test("getTaggedUnionMatcher: a fallback receives the unhandled members", () => { // Arrange const factory = getTaggedUnionMatcher()("kind"); // Act const area = factory( { circle: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "circle"); return 1 as const; }, }, (s) => { // The fallback sees only the members `circle` did not handle. expectTypeOf(s).toEqualTypeOf(); assert.ok(s.kind === "square" || s.kind === "triangle"); return 2 as const; }, ); // Assert expectTypeOf(area).toEqualTypeOf<(shape: Shape) => 1 | 2>(); assert.equal(area({ kind: "circle", radius: 1 }), 1); assert.equal(area({ kind: "square", side: 1 }), 2); assert.equal(area({ kind: "triangle", base: 1, height: 1 }), 2); }); test("getTaggedUnionMatcher: an open discriminant is rejected with a fallback too", () => { // Arrange interface Message { readonly kind: string; readonly text: string; } // Act / Assert — the call below must not compile; a broad tag is unsupported // whether or not a fallback is supplied. // @ts-expect-error `string` tags are not a finite union of literals getTaggedUnionMatcher()("kind")({ info: () => 1 }, () => 2); }); // ============================================================================ // API: getTaggedUnionMatcherW — ✔️ Exhaustive / ❌ ReturnsStrict // ============================================================================ test("getTaggedUnionMatcherW: exhaustive pattern widens to the union of returns", () => { // Arrange const factory = getTaggedUnionMatcherW()("kind"); // Act const describe = factory({ circle: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "circle"); return "round" as const; }, square: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "square"); return 4 as const; }, triangle: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "triangle"); return true as const; }, }); // Assert // ❌ ReturnsStrict: mixed handler returns widen to their union. expectTypeOf(describe).toEqualTypeOf< (shape: Shape) => "round" | 4 | true >(); assert.equal(describe({ kind: "circle", radius: 1 }), "round"); assert.equal(describe({ kind: "square", side: 1 }), 4); assert.equal(describe({ kind: "triangle", base: 1, height: 1 }), true); }); // ============================================================================ // API: getTaggedUnionMatcherW — ❌ Exhaustive (fallback) / ❌ ReturnsStrict // ============================================================================ test("getTaggedUnionMatcherW: a fallback widens gaps into the union", () => { // Arrange const factory = getTaggedUnionMatcherW()("kind"); // Act const matcher = factory({ circle: () => 1 as const }, (s) => { expectTypeOf(s).toEqualTypeOf(); // Returning the member verbatim lets the `Assert` block check the // exact value dispatch passed. return s; }); // Assert expectTypeOf(matcher).toEqualTypeOf< (shape: Shape) => 1 | Square | Triangle >(); assert.equal(matcher({ kind: "circle", radius: 1 }), 1); assert.deepEqual(matcher({ kind: "square", side: 1 }), { kind: "square", side: 1, }); assert.deepEqual(matcher({ kind: "triangle", base: 1, height: 1 }), { kind: "triangle", base: 1, height: 1, }); }); // ============================================================================ // Factory contracts — calls that must not compile // ============================================================================ test("getTaggedUnionMatcher factory rejects patterns outside its contract", () => { // Arrange const factory = getTaggedUnionMatcher()("kind"); // Act / Assert — the calls below must not compile factory({ circle: () => 1, square: () => 2, triangle: () => 3, // @ts-expect-error `hexagon` is not a tag of Shape hexagon: () => 4, }); // @ts-expect-error a gap without a fallback is not exhaustive factory({ circle: () => 1 }); // @ts-expect-error a `string` fallback does not fit the `number` handlers factory({ circle: () => 1 }, () => "x"); // @ts-expect-error a fallback is redundant once the map covers the union factory({ circle: () => 1, square: () => 2, triangle: () => 3 }, () => 0); }); test("getTaggedUnionMatcher factory rejects a non-discriminant key", () => { // Arrange type Mixed = | { readonly id: Date; readonly kind: "a" } | { readonly id: Date; readonly kind: "b" }; // Act / Assert — the calls below must not compile // @ts-expect-error `id` is a common key but its value is not a tag getTaggedUnionMatcher()("id"); getTaggedUnionMatcher()("kind"); }); test("getTaggedUnionMatcher factory rejects non-exhaustive boolean and nullish maps", () => { // Arrange type Result = | { readonly status: true; readonly a: number } | { readonly status: false; readonly b: number }; type Maybe = | { readonly status: null; readonly c: number } | { readonly status: undefined; readonly d: number }; const resultFactory = getTaggedUnionMatcher()("status"); const maybeFactory = getTaggedUnionMatcher()("status"); // Act / Assert — the calls below must not compile // @ts-expect-error only `true` is handled; `false` is not resultFactory({ true: () => 1 }); // @ts-expect-error only `null` is handled; `undefined` is not maybeFactory({ null: () => 1 }); // @ts-expect-error a fallback is redundant once both tags are handled resultFactory({ true: () => 1, false: () => 2 }, () => 0); }); test("getTaggedUnionMatcherW factory rejects patterns outside its contract", () => { // Arrange const factory = getTaggedUnionMatcherW()("kind"); // Act / Assert — the calls below must not compile factory({ circle: () => 1 as const, square: () => 2 as const, triangle: () => 3 as const, // @ts-expect-error `hexagon` is not a tag of Shape hexagon: () => 4 as const, }); // @ts-expect-error a gap without a fallback is not exhaustive factory({ circle: () => 1 as const }); factory( // @ts-expect-error a fallback is redundant once the map covers the union { circle: () => 1 as const, square: () => 2 as const, triangle: () => 3 as const, }, () => 0, ); }); test("getTaggedUnionMatcher: a standalone stringified tag is typed by the member, not its key", () => { // `kind: "true"` is a string tag whose `PatternKey` is `"true"`; inverting // the key must recover the member, not the boolean tag. // Arrange interface StringTag { readonly kind: "true"; readonly a: number; } const factory = getTaggedUnionMatcher()("kind"); // Act const matcher = factory({ true: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "true"); return 1; }, }); // Assert assert.equal(matcher({ kind: "true", a: 1 }), 1); }); test("getTaggedUnionMatcher: a broad tag is rejected", () => { // Arrange interface Message { readonly kind: string; } interface Count { readonly kind: number; } // Act / Assert — the calls below must not compile // @ts-expect-error `string` tags are not a finite union of literals getTaggedUnionMatcher()("kind")({ a: () => 1 }); // @ts-expect-error `number` tags are not a finite union of literals getTaggedUnionMatcher()("kind")({ 1: () => 1 }); // @ts-expect-error the widening factory rejects broad tags too getTaggedUnionMatcherW()("kind")({ a: () => 1 }); }); test("getTaggedUnionMatcher: a value/stringification tag collision is rejected", () => { // Arrange type Colliding = | { readonly kind: "true"; readonly a: number } | { readonly kind: true; readonly b: number }; type NumericColliding = | { readonly kind: "1"; readonly a: number } | { readonly kind: 1; readonly b: number }; // Act / Assert — the calls below must not compile // @ts-expect-error `true` collides with the string tag `"true"` getTaggedUnionMatcher()("kind")({ true: () => 1 }); // @ts-expect-error the number tag `1` collides with the string tag `"1"` getTaggedUnionMatcher()("kind")({ "1": () => 1 }); }); test("getTaggedUnionMatcher: a tag collision is rejected beside legal members", () => { // Act / Assert — the calls below must not compile. Each map is otherwise // complete, so only the collision can make it invalid. type TrueColliding = | { readonly kind: "true"; readonly a: number } | { readonly kind: true; readonly b: number } | { readonly kind: "a"; readonly c: number }; // @ts-expect-error `true` collides with the string tag `"true"` getTaggedUnionMatcher()("kind")({ true: () => 1, a: () => 2, }); type FalseColliding = | { readonly kind: "false"; readonly a: number } | { readonly kind: false; readonly b: number } | { readonly kind: "a"; readonly c: number }; // @ts-expect-error `false` collides with the string tag `"false"` getTaggedUnionMatcher()("kind")({ false: () => 1, a: () => 2, }); type NullColliding = | { readonly kind: "null"; readonly a: number } | { readonly kind: null; readonly b: number } | { readonly kind: "a"; readonly c: number }; // @ts-expect-error `null` collides with the string tag `"null"` getTaggedUnionMatcher()("kind")({ null: () => 1, a: () => 2, }); type UndefinedColliding = | { readonly kind: "undefined"; readonly a: number } | { readonly kind: undefined; readonly b: number } | { readonly kind: "a"; readonly c: number }; // @ts-expect-error `undefined` collides with the string tag `"undefined"` getTaggedUnionMatcher()("kind")({ undefined: () => 1, a: () => 2, }); type NumericColliding = | { readonly kind: "1"; readonly a: number } | { readonly kind: 1; readonly b: number } | { readonly kind: "a"; readonly c: number }; // @ts-expect-error the number tag `1` collides with the string tag `"1"` getTaggedUnionMatcher()("kind")({ "1": () => 1, a: () => 2, }); }); test('getTaggedUnionMatcher: a `"true"` tag and a `false` tag are legal together', () => { // Arrange interface TrueString { readonly kind: "true"; readonly a: number; } interface FalseBare { readonly kind: false; readonly b: number; } interface Extra { readonly kind: "a"; readonly c: number; } type Legal = TrueString | FalseBare | Extra; // Act const matcher = getTaggedUnionMatcher()("kind")({ true: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "true"); return 1; }, false: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, false); return 2; }, a: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "a"); return 3; }, }); // Assert assert.equal(matcher({ kind: "true", a: 1 }), 1); assert.equal(matcher({ kind: false, b: 2 }), 2); assert.equal(matcher({ kind: "a", c: 3 }), 3); }); test('getTaggedUnionMatcher: a `"false"` tag and a `true` tag are legal together', () => { // Arrange interface FalseString { readonly kind: "false"; readonly a: number; } interface TrueBare { readonly kind: true; readonly b: number; } interface Extra { readonly kind: "a"; readonly c: number; } type Legal = FalseString | TrueBare | Extra; // Act const matcher = getTaggedUnionMatcher()("kind")({ false: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "false"); return 1; }, true: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, true); return 2; }, a: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "a"); return 3; }, }); // Assert assert.equal(matcher({ kind: "false", a: 1 }), 1); assert.equal(matcher({ kind: true, b: 2 }), 2); assert.equal(matcher({ kind: "a", c: 3 }), 3); }); test('getTaggedUnionMatcher: a `"null"` tag and an `undefined` tag are legal together', () => { // Arrange interface NullString { readonly kind: "null"; readonly a: number; } interface UndefinedBare { readonly kind: undefined; readonly b: number; } interface Extra { readonly kind: "a"; readonly c: number; } type Legal = NullString | UndefinedBare | Extra; // Act const matcher = getTaggedUnionMatcher()("kind")({ null: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "null"); return 1; }, undefined: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, undefined); return 2; }, a: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "a"); return 3; }, }); // Assert assert.equal(matcher({ kind: "null", a: 1 }), 1); assert.equal(matcher({ kind: undefined, b: 2 }), 2); assert.equal(matcher({ kind: "a", c: 3 }), 3); }); test('getTaggedUnionMatcher: an `"undefined"` tag and a `null` tag are legal together', () => { // Arrange interface UndefinedString { readonly kind: "undefined"; readonly a: number; } interface NullBare { readonly kind: null; readonly b: number; } interface Extra { readonly kind: "a"; readonly c: number; } type Legal = UndefinedString | NullBare | Extra; // Act const matcher = getTaggedUnionMatcher()("kind")({ undefined: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "undefined"); return 1; }, null: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, null); return 2; }, a: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "a"); return 3; }, }); // Assert assert.equal(matcher({ kind: "undefined", a: 1 }), 1); assert.equal(matcher({ kind: null, b: 2 }), 2); assert.equal(matcher({ kind: "a", c: 3 }), 3); }); test('getTaggedUnionMatcher: a `"1"` tag beside other tags is legal', () => { // Arrange interface OneString { readonly kind: "1"; readonly a: number; } interface TwoBare { readonly kind: 2; readonly b: number; } interface ThreeBare { readonly kind: 3; readonly c: number; } interface FourString { readonly kind: "4"; readonly d: number; } type Legal = OneString | TwoBare | ThreeBare | FourString; // Act const matcher = getTaggedUnionMatcher()("kind")({ "1": (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "1"); return 1; }, 2: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, 2); return 2; }, 3: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, 3); return 3; }, "4": (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "4"); return 4; }, }); // Assert assert.equal(matcher({ kind: "1", a: 1 }), 1); assert.equal(matcher({ kind: 2, b: 2 }), 2); assert.equal(matcher({ kind: 3, c: 3 }), 3); assert.equal(matcher({ kind: "4", d: 4 }), 4); }); // The maximum legal tag universe needs one handler per member; the statement // count is inherent to the case. // oxlint-disable-next-line max-statements test("getTaggedUnionMatcher: the maximum legal tag universe is supported", () => { // Arrange — every tag kind, one representation per value, plus extra tags. interface M1 { readonly kind: "true"; readonly a: 1; } interface M2 { readonly kind: false; readonly b: 2; } interface M3 { readonly kind: null; readonly c: 3; } interface M4 { readonly kind: undefined; readonly d: 4; } interface M5 { readonly kind: "1"; readonly e: 5; } interface M6 { readonly kind: 2; readonly f: 6; } interface M7 { readonly kind: 3; readonly g: 7; } interface M8 { readonly kind: "a"; readonly h: 8; } interface M9 { readonly kind: "b"; readonly i: 9; } type Legal = M1 | M2 | M3 | M4 | M5 | M6 | M7 | M8 | M9; // Act const matcher = getTaggedUnionMatcher()("kind")({ true: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "true"); return 1; }, false: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, false); return 2; }, null: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, null); return 3; }, undefined: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, undefined); return 4; }, "1": (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "1"); return 5; }, 2: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, 2); return 6; }, 3: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, 3); return 7; }, a: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "a"); return 8; }, b: (s) => { expectTypeOf(s).toEqualTypeOf(); assert.equal(s.kind, "b"); return 9; }, }); // Assert expectTypeOf(matcher).toEqualTypeOf<(shape: Legal) => number>(); assert.equal(matcher({ kind: "true", a: 1 }), 1); assert.equal(matcher({ kind: false, b: 2 }), 2); assert.equal(matcher({ kind: null, c: 3 }), 3); assert.equal(matcher({ kind: undefined, d: 4 }), 4); assert.equal(matcher({ kind: "1", e: 5 }), 5); assert.equal(matcher({ kind: 2, f: 6 }), 6); assert.equal(matcher({ kind: 3, g: 7 }), 7); assert.equal(matcher({ kind: "a", h: 8 }), 8); assert.equal(matcher({ kind: "b", i: 9 }), 9); }); test("getTaggedUnionMatcher: the maximum illegal tag universe is rejected", () => { // Arrange — every tag paired with its stringification. type Illegal = | { readonly kind: "true"; readonly a: 1 } | { readonly kind: true; readonly a: 1 } | { readonly kind: "false"; readonly b: 2 } | { readonly kind: false; readonly b: 2 } | { readonly kind: "null"; readonly c: 3 } | { readonly kind: null; readonly c: 3 } | { readonly kind: "undefined"; readonly d: 4 } | { readonly kind: undefined; readonly d: 4 } | { readonly kind: "1"; readonly e: 5 } | { readonly kind: 1; readonly e: 5 }; // Act / Assert — the call below must not compile // @ts-expect-error every tag collides with its stringification getTaggedUnionMatcher()("kind")({ true: () => 1, false: () => 2, null: () => 3, undefined: () => 4, "1": () => 5, }); }); // ============================================================================ // Property unions // ============================================================================ test("property union: narrows each handler", () => { // Arrange — one shape, not a union of variants; `color` is a union. interface Paint { readonly color: "red" | "green" | "blue"; readonly value: number; } const factory = getTaggedUnionMatcher()("color"); // Act const describe = factory({ red: (s) => { expectTypeOf(s).toEqualTypeOf<{ readonly color: "red"; readonly value: number; }>(); assert.equal(s.color, "red"); return s.value; }, green: (s) => { expectTypeOf(s).toEqualTypeOf<{ readonly color: "green"; readonly value: number; }>(); assert.equal(s.color, "green"); return s.value; }, blue: (s) => { expectTypeOf(s).toEqualTypeOf<{ readonly color: "blue"; readonly value: number; }>(); assert.equal(s.color, "blue"); return s.value; }, }); // Assert expectTypeOf(describe).toEqualTypeOf<(shape: Paint) => number>(); assert.equal(describe({ color: "red", value: 1 }), 1); assert.equal(describe({ color: "green", value: 2 }), 2); assert.equal(describe({ color: "blue", value: 3 }), 3); }); test("property union: a fallback receives the unhandled values", () => { // Arrange interface Paint { color: "red" | "green" | "blue"; value: number; } const factory = getTaggedUnionMatcher()("color"); // Act const describe = factory( { red: (s) => { expectTypeOf(s).toEqualTypeOf<{ color: "red"; value: number; }>(); assert.equal(s.color, "red"); return 1 as const; }, }, (s) => { expectTypeOf(s).toEqualTypeOf<{ color: "green" | "blue"; value: number; }>(); assert.ok(s.color === "green" || s.color === "blue"); return 2 as const; }, ); // Assert expectTypeOf(describe).toEqualTypeOf<(shape: Paint) => 1 | 2>(); assert.equal(describe({ color: "red", value: 1 }), 1); assert.equal(describe({ color: "green", value: 2 }), 2); assert.equal(describe({ color: "blue", value: 3 }), 2); }); test("property union: a numeric property narrows each handler", () => { // Arrange interface Version { code: 1 | 2 | 3; value: number; } const factory = getTaggedUnionMatcher()("code"); // Act const describe = factory({ 1: (s) => { expectTypeOf(s).toEqualTypeOf<{ code: 1; value: number }>(); assert.equal(s.code, 1); return 1; }, 2: (s) => { expectTypeOf(s).toEqualTypeOf<{ code: 2; value: number }>(); assert.equal(s.code, 2); return 2; }, 3: (s) => { expectTypeOf(s).toEqualTypeOf<{ code: 3; value: number }>(); assert.equal(s.code, 3); return 3; }, }); // Assert expectTypeOf(describe).toEqualTypeOf<(shape: Version) => number>(); assert.equal(describe({ code: 1, value: 10 }), 1); assert.equal(describe({ code: 2, value: 20 }), 2); assert.equal(describe({ code: 3, value: 30 }), 3); }); test("property union: an optional property is required for a defined tag", () => { // Arrange — `type?` is `"x" | undefined`; both handlers must be typed, // and matching `"x"` proves the key is present, so `type` becomes required. interface Maybe { type?: "x"; value: number; } const factory = getTaggedUnionMatcher()("type"); // Act const describe = factory({ x: (s) => { expectTypeOf(s).toEqualTypeOf<{ type: "x"; value: number }>(); assert.equal(s.type, "x"); return 1; }, undefined: (s) => { // The input only allows absence (exactOptionalPropertyTypes), so // the `undefined` tag narrows the key to never-present. expectTypeOf(s).toEqualTypeOf<{ type?: never; value: number }>(); assert.equal(s.type, undefined); return 2; }, }); // Assert expectTypeOf(describe).toEqualTypeOf<(shape: Maybe) => number>(); assert.equal(describe({ type: "x", value: 1 }), 1); assert.equal(describe({ value: 2 }), 2); }); test("property union: an optional fallback receives the undefined tag", () => { // Arrange interface Maybe { type?: "x"; value: number; } const factory = getTaggedUnionMatcher()("type"); // Act const describe = factory( { x: (s) => { expectTypeOf(s).toEqualTypeOf<{ type: "x"; value: number }>(); assert.equal(s.type, "x"); return 1 as const; }, }, (s) => { expectTypeOf(s).toEqualTypeOf<{ type?: never; value: number; }>(); assert.equal(s.type, undefined); return 2 as const; }, ); // Assert expectTypeOf(describe).toEqualTypeOf<(shape: Maybe) => 1 | 2>(); assert.equal(describe({ type: "x", value: 1 }), 1); assert.equal(describe({ value: 2 }), 2); }); test("property union: explicit `undefined` stays distinct from absence", () => { // Arrange — `type?: "x" | undefined` admits an explicit `undefined`, unlike // the bare optional above; the two representations must not be conflated. interface Explicit { type?: "x" | undefined; value: number; } const factory = getTaggedUnionMatcher()("type"); // Act const describe = factory({ x: (s) => { expectTypeOf(s).toEqualTypeOf<{ type: "x"; value: number }>(); assert.equal(s.type, "x"); return 1; }, undefined: (s) => { expectTypeOf(s).toEqualTypeOf<{ type?: undefined; value: number; }>(); assert.equal(s.type, undefined); return 2; }, }); // Assert expectTypeOf(describe).toEqualTypeOf<(shape: Explicit) => number>(); assert.equal(describe({ type: "x", value: 1 }), 1); assert.equal(describe({ type: undefined, value: 2 }), 2); assert.equal(describe({ value: 3 }), 2); }); test("property union: a union-valued member is split, not dropped", () => { // A member whose tag is itself a union sits beside a singleton-tag member. // Selecting members with `Extract>` keeps only the // singleton; the distributive narrowing must keep both sides of the // union-valued member. // Arrange type Mixed = { kind: "a"; a: number } | { kind: "a" | "b"; b: number }; const factory = getTaggedUnionMatcher()("kind"); // Act const describe = factory({ a: (s) => { expectTypeOf(s).toEqualTypeOf< { kind: "a"; a: number } | { kind: "a"; b: number } >(); assert.equal(s.kind, "a"); return 1; }, b: (s) => { expectTypeOf(s).toEqualTypeOf<{ kind: "b"; b: number }>(); assert.equal(s.kind, "b"); return 2; }, }); // Assert expectTypeOf(describe).toEqualTypeOf<(shape: Mixed) => number>(); assert.equal(describe({ kind: "a", a: 1 }), 1); assert.equal(describe({ kind: "a", b: 2 }), 1); assert.equal(describe({ kind: "b", b: 3 }), 2); }); test("property union: getTaggedUnionMatcherW widens the returns", () => { // Arrange interface Paint { color: "red" | "green" | "blue"; value: number; } const factory = getTaggedUnionMatcherW()("color"); // Act const describe = factory({ red: (s) => { expectTypeOf(s).toEqualTypeOf<{ color: "red"; value: number }>(); assert.equal(s.color, "red"); return "r" as const; }, green: (s) => { expectTypeOf(s).toEqualTypeOf<{ color: "green"; value: number }>(); assert.equal(s.color, "green"); return 1 as const; }, blue: (s) => { expectTypeOf(s).toEqualTypeOf<{ color: "blue"; value: number }>(); assert.equal(s.color, "blue"); return true as const; }, }); // Assert expectTypeOf(describe).toEqualTypeOf<(shape: Paint) => "r" | 1 | true>(); assert.equal(describe({ color: "red", value: 1 }), "r"); assert.equal(describe({ color: "green", value: 2 }), 1); assert.equal(describe({ color: "blue", value: 3 }), true); }); test("property union: enforces its contract", () => { // Arrange interface Paint { color: "red" | "green" | "blue"; value: number; } const factory = getTaggedUnionMatcher()("color"); // Act / Assert — the calls below must not compile factory({ red: () => 1, green: () => 2, blue: () => 3, // @ts-expect-error `yellow` is not a value of `color` yellow: () => 4, }); // @ts-expect-error a gap without a fallback is not exhaustive factory({ red: () => 1 }); // @ts-expect-error a fallback is redundant once every value is handled factory({ red: () => 1, green: () => 2, blue: () => 3 }, () => 0); }); test("property union: a broad value in the union is rejected", () => { // Arrange — the property is a union, but not a finite union of literals: a // template literal cannot be proven exhaustive. interface Template { kind: "a" | `x-${number}`; a: number; } // Act / Assert — the calls below must not compile // @ts-expect-error a template literal is not a finite literal getTaggedUnionMatcher