✅ Pin the duplicate-tag union collapse
This commit is contained in:
1 parent
0339a493a2
commit
7ff371c569
1 file changed
+76
@@ -1244,6 +1244,82 @@ test("property union: a collision is rejected beside legal members", () => {
|
||||
getTaggedUnionMatcher<Colliding>()("kind")({ true: () => 1, a: () => 2 });
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Duplicate tags
|
||||
// ============================================================================
|
||||
|
||||
// A tag is a discriminant only while it is unique across the union. Two members
|
||||
// may still share one: the tag set `Tags<T, K>` dedupes, so a single handler is
|
||||
// exhaustive for both and receives their union. This is the documented known
|
||||
// issue (development/library.md § Tagged-union matcher); the tests below pin the
|
||||
// behavior so it cannot change silently.
|
||||
|
||||
test("duplicate tag: members sharing a tag collapse to one handler", () => {
|
||||
// Arrange — `kind` is not a true discriminant: both members carry `"a"`.
|
||||
interface First {
|
||||
readonly kind: "a";
|
||||
readonly first: number;
|
||||
}
|
||||
interface Second {
|
||||
readonly kind: "a";
|
||||
readonly second: string;
|
||||
}
|
||||
type Clashing = First | Second;
|
||||
const factory = getTaggedUnionMatcher<Clashing>()("kind");
|
||||
|
||||
// Act
|
||||
const pick = factory({
|
||||
a: (s) => {
|
||||
// The shared tag cannot be split, so the handler sees both members.
|
||||
expectTypeOf(s).toEqualTypeOf<First | Second>();
|
||||
assert.equal(s.kind, "a");
|
||||
return "first" in s ? s.first : s.second.length;
|
||||
},
|
||||
});
|
||||
|
||||
// Assert — the one `a` key is exhaustive and both members reach it.
|
||||
expectTypeOf(pick).toEqualTypeOf<(shape: Clashing) => number>();
|
||||
assert.equal(pick({ kind: "a", first: 1 }), 1);
|
||||
assert.equal(pick({ kind: "a", second: "abc" }), 3);
|
||||
});
|
||||
|
||||
test("duplicate tag: handling one tag consumes every member that shares it", () => {
|
||||
// Arrange — `"a"` selects two members; `"c"` selects one.
|
||||
type Mixed =
|
||||
| { readonly kind: "a"; readonly a: number }
|
||||
| { readonly kind: "a"; readonly b: string }
|
||||
| { readonly kind: "c"; readonly c: boolean };
|
||||
const factory = getTaggedUnionMatcher<Mixed>()("kind");
|
||||
|
||||
// Act
|
||||
const describe = factory(
|
||||
{
|
||||
a: (s) => {
|
||||
expectTypeOf(s).toEqualTypeOf<
|
||||
| { readonly kind: "a"; readonly a: number }
|
||||
| { readonly kind: "a"; readonly b: string }
|
||||
>();
|
||||
return 1 as const;
|
||||
},
|
||||
},
|
||||
(s) => {
|
||||
// Handling `"a"` removes both of its members, not just one.
|
||||
expectTypeOf(s).toEqualTypeOf<{
|
||||
readonly kind: "c";
|
||||
readonly c: boolean;
|
||||
}>();
|
||||
assert.equal(s.kind, "c");
|
||||
return 2 as const;
|
||||
},
|
||||
);
|
||||
|
||||
// Assert
|
||||
expectTypeOf(describe).toEqualTypeOf<(shape: Mixed) => 1 | 2>();
|
||||
assert.equal(describe({ kind: "a", a: 1 }), 1);
|
||||
assert.equal(describe({ kind: "a", b: "x" }), 1);
|
||||
assert.equal(describe({ kind: "c", c: true }), 2);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Dispatch — runtime behavior
|
||||
// ============================================================================
|
||||
|
||||
Reference in new issue
Block a user