📝 Document the two-argument fallback
Rewrite development/library.md around the new shape: the fallback is a second argument (so it can receive the remainder), `R` needs an inference site in the handler map, `Exact` restores the excess-property check, and the overload order is load-bearing. Record this branch's rejected alternatives (single-object `_`, currying, `this`/HKT, variance/`const`/`NoInfer`/brands) and the redundant- fallback known issue. Add the changelog note and check off the backlog task.
This commit is contained in:
1 parent
0d6a3f1b9b
commit
ae7ffb6fa2
3 files changed
+61
-63
No files matched your search
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
- move the `_` fallback to a second argument typed `Exclude<T, keyof handlers>`, so it receives only the unhandled keys, and infer handler returns from short, unannotated handlers
|
||||||
|
|
||||||
## [0.3.0] - 2026-09-18
|
## [0.3.0] - 2026-09-18
|
||||||
|
|
||||||
- condensed primitive matcher factories down to 2 from formerly 4
|
- condensed primitive matcher factories down to 2 from formerly 4
|
||||||
|
|||||||
+4
-4
@@ -42,10 +42,10 @@ Matcher:
|
|||||||
✔ Clean up: adopt the 3-overload matcher (`src/prototype-ac2.ts`) and delete the prototypes @high @done
|
✔ Clean up: adopt the 3-overload matcher (`src/prototype-ac2.ts`) and delete the prototypes @high @done
|
||||||
→ `getMatcher` / `getMatcherW`, each with overloads `ExhaustiveLoose` → `Fallback` → `Handlers` (order is load-bearing)
|
→ `getMatcher` / `getMatcherW`, each with overloads `ExhaustiveLoose` → `Fallback` → `Handlers` (order is load-bearing)
|
||||||
→ fold into `src/primitive.ts` / the public API; drop `src/prototype*.ts`
|
→ fold into `src/primitive.ts` / the public API; drop `src/prototype*.ts`
|
||||||
☐ `_` should receive only the unhandled `T` keys, not all of `T` @medium
|
✔ `_` should receive only the unhandled `T` keys, not all of `T` @medium @done
|
||||||
→ today `_: (shape: T) => R`; desired `_: (shape: Exclude<T, handledKeys>) => R`
|
→ the fallback is now a second argument: `(handlers, (s) => …)`, `s: Exclude<T, keyof handlers>`
|
||||||
☐ An exhaustive pattern that also carries `_` must be a compile error @medium
|
☐ A fallback for an already-exhaustive handler map must be a compile error @medium
|
||||||
→ `{ a, b, _ }` for `T = "a" | "b"` is accepted today; the redundant `_` should be rejected
|
→ `(handlers, fallback)` with `handlers` covering all of `T` is accepted; the redundant fallback should be rejected (currying would allow the guard)
|
||||||
|
|
||||||
Bugs:
|
Bugs:
|
||||||
✔ TS 7 LSP server logs `context canceled` on stderr at shutdown @done
|
✔ TS 7 LSP server logs `context canceled` on stderr at shutdown @done
|
||||||
|
|||||||
+55
-59
@@ -3,7 +3,7 @@
|
|||||||
The type-level design of the public API and the limitations it carries. The
|
The type-level design of the public API and the limitations it carries. The
|
||||||
user-facing reference is [README § API](../README.md#api).
|
user-facing reference is [README § API](../README.md#api).
|
||||||
|
|
||||||
The matcher below is implemented in `src/primitive.ts` and re-exported from
|
The matcher is implemented in `src/primitive.ts` and re-exported from
|
||||||
`src/index.ts` as `getMatcher` / `getMatcherW`; the rest of the library is
|
`src/index.ts` as `getMatcher` / `getMatcherW`; the rest of the library is
|
||||||
placeholder code.
|
placeholder code.
|
||||||
|
|
||||||
@@ -11,81 +11,77 @@ placeholder code.
|
|||||||
|
|
||||||
#### Decision (2026-09)
|
#### Decision (2026-09)
|
||||||
|
|
||||||
A matcher is built by a factory and applied to a pattern:
|
A factory takes the universe and returns a builder; the builder takes a handler
|
||||||
|
map and an optional fallback:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const matcher = getMatcher<"a" | "b">()({ a: (s) => …, b: (s) => … });
|
const matcher = getMatcher<"a" | "b">()({ a: (s) => …, b: (s) => … });
|
||||||
|
const fallback = getMatcher<"a" | "b" | "c">()({ a: (s) => … }, (s) => …);
|
||||||
```
|
```
|
||||||
|
|
||||||
Whether the pattern is exhaustive or has a fallback is decided **at the call
|
Exhaustive or fallback is decided **at the call site**, by whether the second
|
||||||
site**, by whether it carries `_` — F#'s `| _ ->`. Only the return-strictness
|
argument is present. The fallback's parameter is the remainder
|
||||||
axis remains, so there are two factories:
|
`Exclude<T, keyof Handled>`. Only the return-strictness axis remains, so there
|
||||||
|
are two factories:
|
||||||
|
|
||||||
- `getMatcher` — one common `R`, the best common return type of every handler;
|
- `getMatcher` — one common `R`; the fallback must fit it;
|
||||||
- `getMatcherW` — the union of every handler's return type.
|
- `getMatcherW` — the union `PatternReturns<Handled> | R`.
|
||||||
|
|
||||||
Both are three overloads whose order is load-bearing:
|
Each factory is two overloads whose order is load-bearing:
|
||||||
|
|
||||||
1. `ExhaustiveLoose<R, T>` = `{ [K in T]: UnaryFn<K, R> } & { _?: UnaryFn<T, R> }`
|
1. `Handlers<T, R>` — the exhaustive form, and the contextual type of the
|
||||||
2. `Fallback<R, T>` = `Partial<…> & { _: UnaryFn<T, R> }`
|
handler-map popup;
|
||||||
3. `Handlers<R, T>` — the pure exhaustive shape
|
2. `Handled extends Exact<Partial<Handlers<T, R>>, Handled>` plus
|
||||||
|
`Fallback<T, Handled, R>` — a partial handler map plus the fallback.
|
||||||
|
|
||||||
#### Why
|
#### Why
|
||||||
|
|
||||||
- **Autocomplete reads the first overload, the error reads the last.**
|
- **The fallback is an argument, not a property.** TypeScript fixes a property's
|
||||||
TypeScript takes the first overload signature as the contextual type for the
|
contextual type before it infers its sibling keys, so `_: (s) => …` in the
|
||||||
object-literal popup, and the last for `No overload matches this call`. So
|
handler map can only see all of `T`, never `Exclude<T, keyof Handled>`. A later
|
||||||
`ExhaustiveLoose` first yields the popup `_?, a, b` (T-keys required, `_`
|
argument is contextually typed from inference on an earlier one, so the split
|
||||||
optional) while `Handlers` last yields `Property 'b' is missing`. The two can be
|
is what makes the remainder expressible.
|
||||||
tuned independently.
|
- **Overload order keeps both messages.** #1 supplies the contextual type
|
||||||
- Two factories, not four: the fallback is a pattern _shape_, not a separate API.
|
(`a, b, c`); #2 accepts a partial map once a fallback is present, so its popup
|
||||||
- The widened return union is derived from the pattern's handler types, so it
|
is optional (`a?, b?, c?`). A gap without a fallback is reported against #1.
|
||||||
needs no fourth signature.
|
- **`R` needs an inference site.** `R` inside the `Exact<…>` constraint is not
|
||||||
|
one, so `handlers: Handled & Partial<Handlers<T, R>>` re-adds it; without that
|
||||||
|
`R` collapses to `unknown` when the handler params are inferred.
|
||||||
|
- **`Exact` restores the excess-property check.** TypeScript skips it for a
|
||||||
|
generic constraint, so without `Exact` the handler map accepts keys outside
|
||||||
|
`T`.
|
||||||
|
- Two factories, not four: the fallback is an argument, not a separate API.
|
||||||
|
|
||||||
#### Rejected
|
#### Rejected
|
||||||
|
|
||||||
- **Four factories** (exhaustive and fallback each split by return handling).
|
- **Single-object `_`** (the former shape). `_` sees only all of `T`; the
|
||||||
The exhaustive/fallback axis is expressible as one pattern type; four
|
remainder is not expressible there, and an exhaustive map plus `_` was
|
||||||
signatures duplicate it.
|
accepted.
|
||||||
- **Union merge** — one type `Exhaustive<R,T> | (Partial<…> & { _: … })`,
|
- **Curried handlers-first** — `(handlers)(fallback)`. `Handled` is fixed before
|
||||||
explicit `<T>()`. Type-safe and completable, but TypeScript reports the
|
the second call, so a redundant-fallback guard would work. Rejected: two calls
|
||||||
near-miss union member, so a missing key reads `Property '_' is missing`
|
for the common case.
|
||||||
instead of naming the key. Arm order does not change the report; the overload
|
- **`this` / HKT self-reference.** `this` is post-construction (method bodies,
|
||||||
split does.
|
return positions); a parameter's contextual type is pre-construction.
|
||||||
- **Overload merge with only the exhaustive arm last.** Fixes the missing-key
|
`keyof this` in an interface method is the interface, not the literal.
|
||||||
message, but a wrong `_` parameter is then reported against the exhaustive
|
- **Variance / `const` type parameters / `NoInfer` / `unique symbol` brands /
|
||||||
arm, and `Parameters<typeof factory>` sees only one arm.
|
defaulted type-param guards.** None change inference or evaluation order;
|
||||||
- **Inferred universe** — `match(pattern)` with `T` taken from the keys
|
`in`/`out` on the handler map broke contextual typing outright. `NoInfer`
|
||||||
(exhaustive) or from `_`'s annotated parameter (fallback), via `NoInfer<T>` and
|
specifically leaks into the emitted `.d.ts`, raising the consumer floor to
|
||||||
`_?: never`, split by overloads (a plain union merges inference; measured
|
TypeScript 5.4 (README promises `>= 5.0`).
|
||||||
`T = "_" | "a"`). No explicit `<T>`, and pipe-friendly. Rejected because: with
|
- **Union merge**, **overload merge with only the exhaustive arm last**,
|
||||||
no declared universe the exhaustive popup offers only `_`; an unannotated `_`
|
**inferred universe**, **conditional `RequireKeys`**, **cases-first curried** —
|
||||||
widens `T` to `string | number`; and `NoInfer` leaks into the emitted `.d.ts`,
|
decided against while the API was single-object; their reasons (reported
|
||||||
raising the consumer floor to TypeScript 5.4 (README promises `>= 5.0`).
|
near-miss member, no `_` in the exhaustive popup, `NoInfer`/floor, `keyof P`
|
||||||
- **Conditional `RequireKeys`** — parameter
|
counts optional keys, not pipe-friendly) hold where they still apply.
|
||||||
`P & ("_" extends keyof P ? unknown : Handlers<R, T>)`. Gives the good
|
|
||||||
missing-key message, but `keyof P` counts _optional_ keys: a widened value
|
|
||||||
whose declared type has `_?:` bypasses the completeness check. Demanding a
|
|
||||||
required `_` instead rejects that case but breaks `P` inference — `P` falls back
|
|
||||||
to its constraint and partial literals then demand every key. Typos also need a
|
|
||||||
`NoExtra` guard, whose message degrades to `not assignable to never`.
|
|
||||||
- **Cases-first curried** — `match(["a", "b"])({ a: …, b: … })`. Completion works
|
|
||||||
for exhaustive patterns, and the array is a single source of truth for the
|
|
||||||
runtime list and the union. Rejected as not pipe-friendly; it needs a runtime
|
|
||||||
array; and the single-call form `match(cases, pattern)` cannot infer `R` (the
|
|
||||||
mapped key type `K[number]` stays deferred, so `R` widens to `unknown`).
|
|
||||||
|
|
||||||
#### Known issue
|
#### Known issue
|
||||||
|
|
||||||
- The `_` handler receives **all** of `T`, not the unhandled subset
|
- A redundant fallback is accepted: when the handler map already covers `T`, the
|
||||||
(`Exclude<T, handledKeys>`).
|
fallback is still allowed. The guard would be
|
||||||
- An exhaustive pattern that also carries `_` is accepted; the redundant `_`
|
`Exclude<T, keyof Handled> extends never ? never : unknown`, but the
|
||||||
should be a compile error.
|
conditional is evaluated before `Handled` is inferred; currying is the only
|
||||||
- The widened overloads carry a completeness guard
|
encoding that fixes it (see Rejected).
|
||||||
`keyof P extends T | "_" ? unknown : never`, because TypeScript does not apply
|
- `PatternReturns` must be
|
||||||
the excess-property check to a generic constraint: a generic parameter accepts
|
|
||||||
extra keys, a parameter typed as a concrete object type does not.
|
|
||||||
`PatternReturns` must be
|
|
||||||
`ReturnType<Extract<ValueOf<P>, (...args: never[]) => unknown>>` so it survives
|
`ReturnType<Extract<ValueOf<P>, (...args: never[]) => unknown>>` so it survives
|
||||||
the closed, partly-optional `P` constraints.
|
the closed, partly-optional `P` constraints.
|
||||||
- `Parameters<typeof factory>[0]` resolves only the **last** overload, so it is
|
- `Parameters<typeof factory>[0]` resolves only the **last** overload, so it is
|
||||||
|
|||||||
Reference in new issue
Block a user