✨ Reject a fallback for an exhaustive map

A fallback paired with a handler map that already covers `T` was still
accepted, even though its remainder is empty. Fold the guard into
`Handled`'s own (F-bounded) constraint so it is checked after inference;
a conditional in the fallback parameter is evaluated while `Handled` is
still its constraint and rejects every partial map whose handler callbacks
need contextual typing.
This commit is contained in:
tmu committed 2026-09-20 22:05:40 +00:00
1 parent 7e7f716424
commit f27966e803
5 files changed
+65 -14

No files matched your search

+22
View File
@@ -309,6 +309,26 @@ test("getMatcher factory rejects patterns outside its contract", () => {
factory({ a: () => 1 });
// @ts-expect-error a `string` fallback does not fit the `number` handler
factory({ a: () => 1 }, () => "x");
// @ts-expect-error a fallback is redundant once the map covers the universe
factory({ a: () => 1, b: () => 2 }, () => 0);
});
test("getMatcher: an open universe keeps the fallback's remainder open", () => {
// Arrange
const factory = getMatcher<string>();
// Act
const matcher = factory({ a: () => 1 }, (s) => {
// The map's literal keys do not close an open universe, so the
// remainder stays `string` and the fallback is not redundant.
expectTypeOf(s).toEqualTypeOf<string>();
return 2 as const;
});
// Assert
expectTypeOf(matcher).toEqualTypeOf<(shape: string) => number>();
assert.equal(matcher("a"), 1);
assert.equal(matcher("b"), 2);
});
test("getMatcherW factory rejects patterns outside its contract", () => {
@@ -324,6 +344,8 @@ test("getMatcherW factory rejects patterns outside its contract", () => {
});
// @ts-expect-error a gap without a `_` fallback is not exhaustive
factory({ x: () => 1 as const });
// @ts-expect-error a fallback is redundant once the map covers the universe
factory({ x: () => 1 as const, y: () => 2 as const }, () => 0);
});
// ============================================================================