Compare commits
2
Commits
7e7f716424
...
c32fe08c73
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c32fe08c73 | ||
|
|
f27966e803 |
No files matched your search
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- reject a fallback when the handler map already covers the universe
|
||||
|
||||
## [0.4.0] - 2026-09-20
|
||||
|
||||
- move the `_` fallback out of handlers
|
||||
|
||||
+2
-2
@@ -44,8 +44,8 @@ Matcher:
|
||||
→ fold into `src/primitive.ts` / the public API; drop `src/prototype*.ts`
|
||||
✔ `_` should receive only the unhandled `T` keys, not all of `T` @medium @done
|
||||
→ the fallback is now a second argument: `(handlers, (s) => …)`, `s: Exclude<T, keyof handlers>`
|
||||
☐ A fallback for an already-exhaustive handler map must be a compile error @medium
|
||||
→ `(handlers, fallback)` with `handlers` covering all of `T` is accepted; the redundant fallback should be rejected (currying would allow the guard)
|
||||
✔ A fallback for an already-exhaustive handler map must be a compile error @medium @done
|
||||
→ rejected by an F-bounded constraint on `Handled` (checked *after* inference); a conditional in the fallback parameter is evaluated too early and breaks contextual typing
|
||||
|
||||
Bugs:
|
||||
✔ TS 7 LSP server logs `context canceled` on stderr at shutdown @done
|
||||
|
||||
+14
-10
@@ -31,8 +31,9 @@ Each factory is two overloads whose order is load-bearing:
|
||||
|
||||
1. `Handlers<T, R>` — the exhaustive form, and the contextual type of the
|
||||
handler-map popup;
|
||||
2. `Handled extends Exact<Partial<Handlers<T, R>>, Handled>` plus
|
||||
`Fallback<T, Handled, R>` — a partial handler map plus the fallback.
|
||||
2. `Handled extends Exact<Partial<Handlers<T, R>>, Handled>` intersected with
|
||||
`MustBePartial<T, Handled>`, plus `Fallback<T, Handled, R>` — a partial
|
||||
handler map plus the fallback, rejected when the map already covers `T`.
|
||||
|
||||
#### Why
|
||||
|
||||
@@ -41,6 +42,14 @@ Each factory is two overloads whose order is load-bearing:
|
||||
handler map can only see all of `T`, never `Exclude<T, keyof Handled>`. A later
|
||||
argument is contextually typed from inference on an earlier one, so the split
|
||||
is what makes the remainder expressible.
|
||||
- **The redundant-fallback guard is an F-bounded constraint.** A map that
|
||||
already covers `T` plus a fallback is rejected by folding
|
||||
`MustBePartial<T, Handled>` into `Handled`'s own constraint. The guard is
|
||||
checked _after_ `Handled` is inferred, so the contextual pass that types the
|
||||
handler callbacks survives. The obvious conditional
|
||||
`Exclude<T, keyof Handled> extends never ? …` in the fallback's parameter
|
||||
type is evaluated while `Handled` is still its constraint and rejects every
|
||||
partial map whose callbacks are context-sensitive.
|
||||
- **Overload order keeps both messages.** #1 supplies the contextual type
|
||||
(`a, b, c`); #2 accepts a partial map once a fallback is present, so its popup
|
||||
is optional (`a?, b?, c?`). A gap without a fallback is reported against #1.
|
||||
@@ -57,9 +66,9 @@ Each factory is two overloads whose order is load-bearing:
|
||||
- **Single-object `_`** (the former shape). `_` sees only all of `T`; the
|
||||
remainder is not expressible there, and an exhaustive map plus `_` was
|
||||
accepted.
|
||||
- **Curried handlers-first** — `(handlers)(fallback)`. `Handled` is fixed before
|
||||
the second call, so a redundant-fallback guard would work. Rejected: two calls
|
||||
for the common case.
|
||||
- **Curried handlers-first** — `(handlers)(fallback)`. Rejected: two calls for
|
||||
the common case. It is not needed for the redundant-fallback guard, which the
|
||||
F-bounded constraint already provides (see Why).
|
||||
- **`this` / HKT self-reference.** `this` is post-construction (method bodies,
|
||||
return positions); a parameter's contextual type is pre-construction.
|
||||
`keyof this` in an interface method is the interface, not the literal.
|
||||
@@ -76,11 +85,6 @@ Each factory is two overloads whose order is load-bearing:
|
||||
|
||||
#### Known issue
|
||||
|
||||
- A redundant fallback is accepted: when the handler map already covers `T`, the
|
||||
fallback is still allowed. The guard would be
|
||||
`Exclude<T, keyof Handled> extends never ? never : unknown`, but the
|
||||
conditional is evaluated before `Handled` is inferred; currying is the only
|
||||
encoding that fixes it (see Rejected).
|
||||
- `PatternReturns` must be
|
||||
`ReturnType<Extract<ValueOf<P>, (...args: never[]) => unknown>>` so it survives
|
||||
the closed, partly-optional `P` constraints.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
|
||||
+25
-2
@@ -21,6 +21,21 @@ type Fallback<T extends string | number, Handled, R> = UnaryFn<
|
||||
R
|
||||
>;
|
||||
|
||||
// A fallback is redundant once the handler map covers `T`. The guard is folded
|
||||
// into `Handled`'s own (self-referential) constraint so it is checked *after*
|
||||
// inference; a conditional in the fallback's parameter type is evaluated while
|
||||
// `Handled` is still its constraint and would reject context-sensitive partial
|
||||
// maps. That placement also fixes where the diagnostic lands: the constraint
|
||||
// failure is reported on the argument that inferred `Handled` (the handler
|
||||
// map), so the required property is spelled as the message instead of relying
|
||||
// on its position. See development/library.md.
|
||||
interface RedundantFallback {
|
||||
readonly "every case is already handled, so the fallback is redundant": never;
|
||||
}
|
||||
type MustBePartial<T extends string | number, Handled> = T extends keyof Handled
|
||||
? RedundantFallback
|
||||
: unknown;
|
||||
|
||||
// TypeScript does not apply the excess-property check to a generic constraint,
|
||||
// so `Exact` restores it for the generic forms: a handler map can otherwise
|
||||
// carry keys outside `T`.
|
||||
@@ -31,7 +46,11 @@ type Fallback<T extends string | number, Handled, R> = UnaryFn<
|
||||
// #2 Fallback (last) -> accepts a partial handler map plus a fallback
|
||||
interface MatcherStrict<T extends string | number> {
|
||||
<R>(handlers: Handlers<T, R>): UnaryFn<T, R>;
|
||||
<R, Handled extends Exact<Partial<Handlers<T, R>>, Handled>>(
|
||||
<
|
||||
R,
|
||||
Handled extends Exact<Partial<Handlers<T, R>>, Handled> &
|
||||
MustBePartial<T, Handled>,
|
||||
>(
|
||||
handlers: Handled & Partial<Handlers<T, R>>,
|
||||
fallback: Fallback<T, Handled, R>,
|
||||
): UnaryFn<T, R>;
|
||||
@@ -46,7 +65,11 @@ interface MatcherWidening<T extends string | number> {
|
||||
<P extends Exact<Handlers<T, unknown>, P>>(
|
||||
handlers: P,
|
||||
): UnaryFn<T, PatternReturns<P>>;
|
||||
<R, Handled extends Exact<Partial<Handlers<T, unknown>>, Handled>>(
|
||||
<
|
||||
R,
|
||||
Handled extends Exact<Partial<Handlers<T, unknown>>, Handled> &
|
||||
MustBePartial<T, Handled>,
|
||||
>(
|
||||
handlers: Handled,
|
||||
fallback: Fallback<T, Handled, R>,
|
||||
): UnaryFn<T, PatternReturns<Handled> | R>;
|
||||
|
||||
Reference in new issue
Block a user