🔀 Merge chore/duplicate-tag-union-test into main
This commit is contained in:
commit
76327a8c47
4 files changed
+88
-4
No files matched your search
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
- pin the duplicate-tag union collapse (members sharing a tag dispatch through
|
||||||
|
one handler) with tests
|
||||||
|
- track a TypeScript 5.0 baseline in the backlog
|
||||||
|
|
||||||
## [0.8.0] - 2026-09-23
|
## [0.8.0] - 2026-09-23
|
||||||
|
|
||||||
- reject a universe that mixes a literal with a broad type (e.g.
|
- reject a universe that mixes a literal with a broad type (e.g.
|
||||||
|
|||||||
@@ -44,3 +44,4 @@ Maintenance:
|
|||||||
☐ Add a minimal dir-listing webserver to the gitea docker setup for serving landing page (reuse existing reverse proxy)
|
☐ Add a minimal dir-listing webserver to the gitea docker setup for serving landing page (reuse existing reverse proxy)
|
||||||
☐ CI writes landing page to a shared volume keyed by project + tag (e.g. `/landing/tiny-pattern-ts/<tag>/`)
|
☐ CI writes landing page to a shared volume keyed by project + tag (e.g. `/landing/tiny-pattern-ts/<tag>/`)
|
||||||
☐ Browse to `…/tiny-pattern-ts/index.html` in the browser
|
☐ Browse to `…/tiny-pattern-ts/index.html` in the browser
|
||||||
|
☐ Add testing with TypeScript 5.0 baseline in CI
|
||||||
@@ -245,10 +245,13 @@ The key is a separate call because `K` is inferred from its literal argument and
|
|||||||
|
|
||||||
#### Known issue
|
#### Known issue
|
||||||
|
|
||||||
- A member's tag must be unique across the union; two members with the same tag
|
- A tag need not be unique across the union. Two members sharing one is not a
|
||||||
collapse to a union under one handler. A tag colliding with its
|
soundness hole: they select a single runtime key, so one handler receiving
|
||||||
stringification (`true | "true"`) is rejected by the universe gate — see
|
their union is the only correct behavior — the key is simply not a
|
||||||
§ Supported universes.
|
discriminant. The gate rejects only the distinct-value collision
|
||||||
|
(`true | "true"`), where two values share a key and `Member` can no longer
|
||||||
|
invert it; see § Supported universes. Pinned by the duplicate-tag tests in
|
||||||
|
`src/tagged-union.test.ts`.
|
||||||
|
|
||||||
## Primitive universe
|
## Primitive universe
|
||||||
|
|
||||||
|
|||||||
@@ -1244,6 +1244,82 @@ test("property union: a collision is rejected beside legal members", () => {
|
|||||||
getTaggedUnionMatcher<Colliding>()("kind")({ true: () => 1, a: () => 2 });
|
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
|
// Dispatch — runtime behavior
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Reference in new issue
Block a user