Compare commits
12
Commits
0.3.0
...
c32fe08c73
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c32fe08c73 | ||
|
|
f27966e803 | ||
|
|
7e7f716424 | ||
|
|
62db7de537 | ||
|
|
ff823b3cba | ||
|
|
eeb831b717 | ||
|
|
ae7ffb6fa2 | ||
|
|
0d6a3f1b9b | ||
|
|
8615723c64 | ||
|
|
935e82d205 | ||
|
|
c7223dbe69 | ||
|
|
2d3577716d |
No files matched your search
@@ -42,7 +42,9 @@ Never start a long-lived / blocking process such as `npm run watch`. It runs unt
|
|||||||
|
|
||||||
### Working on tasks
|
### Working on tasks
|
||||||
|
|
||||||
- **Task with subtasks** (a task that has indented children): create the branch with `npm run create:branch -- <prefix>/<desc>`, inferring the prefix from the task content (`feature/…` / `fix/…` / `chore/…`) — do not hand-write `git switch -c`, the command enforces the clean-tree / current-`main` / green-baseline precondition. Work on each subtask with commits, then present a concise handover for the user to review. Use this fixed shape:
|
Every task — with or without subtasks — goes through the complete branching model:
|
||||||
|
|
||||||
|
- Create the branch with `npm run create:branch -- <prefix>/<desc>`, inferring the prefix from the task content (`feature/…` / `fix/…` / `chore/…`) — do not hand-write `git switch -c`, the command enforces the clean-tree / current-`main` / green-baseline precondition. Work with commits (each subtask gets one or more), then present a concise handover for the user to review. Use this fixed shape:
|
||||||
|
|
||||||
```md
|
```md
|
||||||
## Handover — <branch>
|
## Handover — <branch>
|
||||||
@@ -54,9 +56,7 @@ Never start a long-lived / blocking process such as `npm run watch`. It runs unt
|
|||||||
|
|
||||||
Once the user has no further objections, merge back: `npm run create:finish` (on the branch — it merges `--no-ff`, runs `npm run verify`, and deletes the branch). The branching model is documented in [CONTRIBUTING.md § Branching model](./CONTRIBUTING.md#branching-model).
|
Once the user has no further objections, merge back: `npm run create:finish` (on the branch — it merges `--no-ff`, runs `npm run verify`, and deletes the branch). The branching model is documented in [CONTRIBUTING.md § Branching model](./CONTRIBUTING.md#branching-model).
|
||||||
|
|
||||||
- **Leaf task** (no indented children): implement on the current branch and commit.
|
Follow [CONTRIBUTING.md § Testing discipline (type-driven)](./CONTRIBUTING.md#testing-discipline-type-driven) throughout.
|
||||||
|
|
||||||
In both cases, follow [CONTRIBUTING.md § Testing discipline (type-driven)](./CONTRIBUTING.md#testing-discipline-type-driven). Each subtask gets one or more commits.
|
|
||||||
|
|
||||||
## Read these
|
## Read these
|
||||||
|
|
||||||
|
|||||||
+8
-1
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
- reject a fallback when the handler map already covers the universe
|
||||||
|
|
||||||
|
## [0.4.0] - 2026-09-20
|
||||||
|
|
||||||
|
- move the `_` fallback out of 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
|
||||||
@@ -59,7 +65,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- basic setup
|
- basic setup
|
||||||
|
|
||||||
[Unreleased]: https://gitea.e1nsnull.de/tmu/tiny-pattern-ts/compare/0.3.0...main
|
[Unreleased]: https://gitea.e1nsnull.de/tmu/tiny-pattern-ts/compare/0.4.0...main
|
||||||
|
[0.4.0]: https://gitea.e1nsnull.de/tmu/tiny-pattern-ts/compare/0.3.0...0.4.0
|
||||||
[0.3.0]: https://gitea.e1nsnull.de/tmu/tiny-pattern-ts/compare/0.2.0...0.3.0
|
[0.3.0]: https://gitea.e1nsnull.de/tmu/tiny-pattern-ts/compare/0.2.0...0.3.0
|
||||||
[0.2.0]: https://gitea.e1nsnull.de/tmu/tiny-pattern-ts/compare/0.1.8...0.2.0
|
[0.2.0]: https://gitea.e1nsnull.de/tmu/tiny-pattern-ts/compare/0.1.8...0.2.0
|
||||||
[0.1.8]: https://gitea.e1nsnull.de/tmu/tiny-pattern-ts/compare/0.1.7...0.1.8
|
[0.1.8]: https://gitea.e1nsnull.de/tmu/tiny-pattern-ts/compare/0.1.7...0.1.8
|
||||||
|
|||||||
+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 @done
|
||||||
→ `{ a, b, _ }` for `T = "a" | "b"` is accepted today; the redundant `_` should be rejected
|
→ 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:
|
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
|
||||||
|
|||||||
+59
-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,81 @@ 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>` 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
|
#### 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.
|
- **The redundant-fallback guard is an F-bounded constraint.** A map that
|
||||||
- Two factories, not four: the fallback is a pattern _shape_, not a separate API.
|
already covers `T` plus a fallback is rejected by folding
|
||||||
- The widened return union is derived from the pattern's handler types, so it
|
`MustBePartial<T, Handled>` into `Handled`'s own constraint. The guard is
|
||||||
needs no fourth signature.
|
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.
|
||||||
|
- **`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)`. Rejected: two calls for
|
||||||
explicit `<T>()`. Type-safe and completable, but TypeScript reports the
|
the common case. It is not needed for the redundant-fallback guard, which the
|
||||||
near-miss union member, so a missing key reads `Property '_' is missing`
|
F-bounded constraint already provides (see Why).
|
||||||
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
|
- `PatternReturns` must be
|
||||||
(`Exclude<T, handledKeys>`).
|
|
||||||
- An exhaustive pattern that also carries `_` is accepted; the redundant `_`
|
|
||||||
should be a compile error.
|
|
||||||
- The widened overloads carry a completeness guard
|
|
||||||
`keyof P extends T | "_" ? unknown : never`, because TypeScript does not apply
|
|
||||||
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
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "tiny-pattern-ts",
|
"name": "tiny-pattern-ts",
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "tiny-pattern-ts",
|
"name": "tiny-pattern-ts",
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"type-fest": "^5.9.0"
|
"type-fest": "^5.9.0"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tiny-pattern-ts",
|
"name": "tiny-pattern-ts",
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"description": "Pattern matching for TypeScript/ESM environments (F#-style, not regex)",
|
"description": "Pattern matching for TypeScript/ESM environments (F#-style, not regex)",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"adt",
|
"adt",
|
||||||
|
|||||||
+155
-99
@@ -22,11 +22,11 @@ test("getMatcher: exhaustive pattern infers one common return type", () => {
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
const matcher = factory({
|
const matcher = factory({
|
||||||
a: (s): number => {
|
a: (s) => {
|
||||||
expectTypeOf(s).toEqualTypeOf<"a">();
|
expectTypeOf(s).toEqualTypeOf<"a">();
|
||||||
return 1;
|
return 1;
|
||||||
},
|
},
|
||||||
b: (s): 1 | 2 => {
|
b: (s) => {
|
||||||
expectTypeOf(s).toEqualTypeOf<"b">();
|
expectTypeOf(s).toEqualTypeOf<"b">();
|
||||||
return 2;
|
return 2;
|
||||||
},
|
},
|
||||||
@@ -69,19 +69,19 @@ test("getMatcher: dispatches on mixed string and numeric keys", () => {
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
const matcher = factory({
|
const matcher = factory({
|
||||||
a: (s): number => {
|
a: (s) => {
|
||||||
expectTypeOf(s).toEqualTypeOf<"a">();
|
expectTypeOf(s).toEqualTypeOf<"a">();
|
||||||
return 1;
|
return 1;
|
||||||
},
|
},
|
||||||
b: (s): 1 | 2 => {
|
b: (s) => {
|
||||||
expectTypeOf(s).toEqualTypeOf<"b">();
|
expectTypeOf(s).toEqualTypeOf<"b">();
|
||||||
return 2;
|
return 2;
|
||||||
},
|
},
|
||||||
1: (n): number => {
|
1: (n) => {
|
||||||
expectTypeOf(n).toEqualTypeOf<1>();
|
expectTypeOf(n).toEqualTypeOf<1>();
|
||||||
return 10;
|
return 10;
|
||||||
},
|
},
|
||||||
2: (n): number => {
|
2: (n) => {
|
||||||
expectTypeOf(n).toEqualTypeOf<2>();
|
expectTypeOf(n).toEqualTypeOf<2>();
|
||||||
return 20;
|
return 20;
|
||||||
},
|
},
|
||||||
@@ -99,22 +99,24 @@ test("getMatcher: dispatches on mixed string and numeric keys", () => {
|
|||||||
// API: getMatcher — ❌ Exhaustive (fallback) / ✔️ ReturnsStrict
|
// API: getMatcher — ❌ Exhaustive (fallback) / ✔️ ReturnsStrict
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
test("getMatcher: a `_` fallback makes the universe keys optional", () => {
|
test("getMatcher: a `_` fallback receives the unhandled keys", () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const factory = getMatcher<"a" | "b" | "c">();
|
const factory = getMatcher<"a" | "b" | "c">();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const matcher = factory({
|
const matcher = factory(
|
||||||
a: (s): 1 | 2 => {
|
{
|
||||||
|
a: (s) => {
|
||||||
expectTypeOf(s).toEqualTypeOf<"a">();
|
expectTypeOf(s).toEqualTypeOf<"a">();
|
||||||
return 1;
|
return 1 as const;
|
||||||
},
|
},
|
||||||
_: (s): 1 | 2 => {
|
|
||||||
// The fallback sees the whole union, not a single literal.
|
|
||||||
expectTypeOf(s).toEqualTypeOf<"a" | "b" | "c">();
|
|
||||||
return 2;
|
|
||||||
},
|
},
|
||||||
});
|
(s) => {
|
||||||
|
// The fallback sees only the keys `a` did not handle.
|
||||||
|
expectTypeOf(s).toEqualTypeOf<"b" | "c">();
|
||||||
|
return 2 as const;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b" | "c") => 1 | 2>();
|
expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b" | "c") => 1 | 2>();
|
||||||
@@ -125,47 +127,27 @@ test("getMatcher: a `_` fallback makes the universe keys optional", () => {
|
|||||||
assert.equal(matcher("c"), 2);
|
assert.equal(matcher("c"), 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getMatcher: a `_` fallback also accepts an exhaustive pattern", () => {
|
|
||||||
// Arrange
|
|
||||||
const factory = getMatcher<"a" | "b">();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const matcher = factory({
|
|
||||||
a: (s) => {
|
|
||||||
expectTypeOf(s).toEqualTypeOf<"a">();
|
|
||||||
return "A";
|
|
||||||
},
|
|
||||||
b: (s) => {
|
|
||||||
expectTypeOf(s).toEqualTypeOf<"b">();
|
|
||||||
return "B";
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b") => string>();
|
|
||||||
assert.equal(matcher("a"), "A");
|
|
||||||
assert.equal(matcher("b"), "B");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getMatcher: a `_` fallback routes mixed string and numeric gaps", () => {
|
test("getMatcher: a `_` fallback routes mixed string and numeric gaps", () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const factory = getMatcher<"a" | "b" | 1 | 2>();
|
const factory = getMatcher<"a" | "b" | 1 | 2>();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const matcher = factory({
|
const matcher = factory(
|
||||||
a: (s): string => {
|
{
|
||||||
|
a: (s) => {
|
||||||
expectTypeOf(s).toEqualTypeOf<"a">();
|
expectTypeOf(s).toEqualTypeOf<"a">();
|
||||||
return "A";
|
return "A";
|
||||||
},
|
},
|
||||||
1: (n): string => {
|
1: (n) => {
|
||||||
expectTypeOf(n).toEqualTypeOf<1>();
|
expectTypeOf(n).toEqualTypeOf<1>();
|
||||||
return "one";
|
return "one";
|
||||||
},
|
},
|
||||||
_: (s): string => {
|
},
|
||||||
expectTypeOf(s).toEqualTypeOf<"a" | "b" | 1 | 2>();
|
(s) => {
|
||||||
|
expectTypeOf(s).toEqualTypeOf<"b" | 2>();
|
||||||
return "fallback";
|
return "fallback";
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b" | 1 | 2) => string>();
|
expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b" | 1 | 2) => string>();
|
||||||
@@ -247,17 +229,19 @@ test("getMatcherW: a `_` fallback widens gaps into the union", () => {
|
|||||||
const factory = getMatcherW<"x" | "y" | "z">();
|
const factory = getMatcherW<"x" | "y" | "z">();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const matcher = factory({
|
const matcher = factory(
|
||||||
|
{
|
||||||
x: (s) => {
|
x: (s) => {
|
||||||
expectTypeOf(s).toEqualTypeOf<"x">();
|
expectTypeOf(s).toEqualTypeOf<"x">();
|
||||||
return 1 as const;
|
return 1 as const;
|
||||||
},
|
},
|
||||||
_: (s) => {
|
},
|
||||||
// The fallback sees the whole union, not a single literal.
|
(s) => {
|
||||||
expectTypeOf(s).toEqualTypeOf<"x" | "y" | "z">();
|
// The fallback sees only the keys `x` did not handle.
|
||||||
|
expectTypeOf(s).toEqualTypeOf<"y" | "z">();
|
||||||
return "fallback" as const;
|
return "fallback" as const;
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expectTypeOf(matcher).toEqualTypeOf<
|
expectTypeOf(matcher).toEqualTypeOf<
|
||||||
@@ -275,7 +259,8 @@ test("getMatcherW: a `_` fallback widens mixed string and numeric returns", () =
|
|||||||
const factory = getMatcherW<"a" | "b" | 1 | 2>();
|
const factory = getMatcherW<"a" | "b" | 1 | 2>();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const matcher = factory({
|
const matcher = factory(
|
||||||
|
{
|
||||||
a: (s) => {
|
a: (s) => {
|
||||||
expectTypeOf(s).toEqualTypeOf<"a">();
|
expectTypeOf(s).toEqualTypeOf<"a">();
|
||||||
return "A" as const;
|
return "A" as const;
|
||||||
@@ -284,11 +269,12 @@ test("getMatcherW: a `_` fallback widens mixed string and numeric returns", () =
|
|||||||
expectTypeOf(n).toEqualTypeOf<1>();
|
expectTypeOf(n).toEqualTypeOf<1>();
|
||||||
return 10 as const;
|
return 10 as const;
|
||||||
},
|
},
|
||||||
_: (s) => {
|
},
|
||||||
expectTypeOf(s).toEqualTypeOf<"a" | "b" | 1 | 2>();
|
(s) => {
|
||||||
|
expectTypeOf(s).toEqualTypeOf<"b" | 2>();
|
||||||
return "fallback" as const;
|
return "fallback" as const;
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
// ❌ ReturnsStrict: mixed handler returns widen to their union.
|
// ❌ ReturnsStrict: mixed handler returns widen to their union.
|
||||||
@@ -301,28 +287,6 @@ test("getMatcherW: a `_` fallback widens mixed string and numeric returns", () =
|
|||||||
assert.equal(matcher(2), "fallback");
|
assert.equal(matcher(2), "fallback");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getMatcherW: a `_` fallback also accepts an exhaustive pattern", () => {
|
|
||||||
// Arrange
|
|
||||||
const factory = getMatcherW<"x" | "y">();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const matcher = factory({
|
|
||||||
x: (s) => {
|
|
||||||
expectTypeOf(s).toEqualTypeOf<"x">();
|
|
||||||
return 1 as const;
|
|
||||||
},
|
|
||||||
y: (s) => {
|
|
||||||
expectTypeOf(s).toEqualTypeOf<"y">();
|
|
||||||
return "two" as const;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expectTypeOf(matcher).toEqualTypeOf<(shape: "x" | "y") => 1 | "two">();
|
|
||||||
assert.equal(matcher("x"), 1);
|
|
||||||
assert.equal(matcher("y"), "two");
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Factory contracts — calls that must not compile
|
// Factory contracts — calls that must not compile
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -341,8 +305,30 @@ test("getMatcher factory rejects patterns outside its contract", () => {
|
|||||||
// @ts-expect-error `c` is not part of the universe `"a" | "b"`
|
// @ts-expect-error `c` is not part of the universe `"a" | "b"`
|
||||||
c: () => 3,
|
c: () => 3,
|
||||||
});
|
});
|
||||||
// @ts-expect-error a gap without `_` is not exhaustive
|
// @ts-expect-error a gap without a `_` fallback is not exhaustive
|
||||||
factory({ a: () => 1 });
|
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", () => {
|
test("getMatcherW factory rejects patterns outside its contract", () => {
|
||||||
@@ -350,14 +336,31 @@ test("getMatcherW factory rejects patterns outside its contract", () => {
|
|||||||
const factory = getMatcherW<"x" | "y">();
|
const factory = getMatcherW<"x" | "y">();
|
||||||
|
|
||||||
// Act / Assert — the calls below must not compile
|
// Act / Assert — the calls below must not compile
|
||||||
// @ts-expect-error `z` is not part of the universe `"x" | "y"`
|
|
||||||
factory({
|
factory({
|
||||||
x: () => 1 as const,
|
x: () => 1 as const,
|
||||||
y: () => 2 as const,
|
y: () => 2 as const,
|
||||||
|
// @ts-expect-error `z` is not part of the universe `"x" | "y"`
|
||||||
z: () => 3 as const,
|
z: () => 3 as const,
|
||||||
});
|
});
|
||||||
// @ts-expect-error a gap without `_` is not exhaustive
|
// @ts-expect-error a gap without a `_` fallback is not exhaustive
|
||||||
factory({ x: () => 1 as const });
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Dispatch — runtime behavior
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
test("getMatcher: an unhandled shape throws without a fallback", () => {
|
||||||
|
// Arrange — an open universe types its handler map as an index signature,
|
||||||
|
// so the type system cannot prove the runtime map is exhaustive.
|
||||||
|
const handlers: Record<string, () => number> = { a: () => 1 };
|
||||||
|
const matcher = getMatcher<string>()(handlers);
|
||||||
|
|
||||||
|
// Act / Assert
|
||||||
|
assert.equal(matcher("a"), 1);
|
||||||
|
assert.throws(() => matcher("b"), /Unhandled shape: b/);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -374,11 +377,19 @@ const UNIVERSE = `"a" | "b" | "c"`;
|
|||||||
|
|
||||||
// One session per probe: the tests share no language-server state (open
|
// One session per probe: the tests share no language-server state (open
|
||||||
// documents, project membership), so they pass in any order.
|
// documents, project membership), so they pass in any order.
|
||||||
const labelsFor = (
|
interface LabelsProbe {
|
||||||
name: string,
|
readonly name: string;
|
||||||
factory: "getMatcher" | "getMatcherW",
|
readonly factory: "getMatcher" | "getMatcherW";
|
||||||
body: string,
|
readonly body: string;
|
||||||
): Promise<readonly string[]> => {
|
readonly tail?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelsFor = ({
|
||||||
|
name,
|
||||||
|
factory,
|
||||||
|
body,
|
||||||
|
tail = "",
|
||||||
|
}: LabelsProbe): Promise<readonly string[]> => {
|
||||||
const session = new LspSession(REPO_ROOT);
|
const session = new LspSession(REPO_ROOT);
|
||||||
const target: CompletionTarget = {
|
const target: CompletionTarget = {
|
||||||
file: `src/__autocomplete_${name}.ts`,
|
file: `src/__autocomplete_${name}.ts`,
|
||||||
@@ -386,7 +397,7 @@ const labelsFor = (
|
|||||||
`import { ${factory} } from "./index.ts";`,
|
`import { ${factory} } from "./index.ts";`,
|
||||||
`const m = ${factory}<${UNIVERSE}>()({`,
|
`const m = ${factory}<${UNIVERSE}>()({`,
|
||||||
body,
|
body,
|
||||||
"});",
|
`}${tail});`,
|
||||||
"",
|
"",
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
};
|
};
|
||||||
@@ -396,16 +407,20 @@ const labelsFor = (
|
|||||||
.finally(() => session.close());
|
.finally(() => session.close());
|
||||||
};
|
};
|
||||||
|
|
||||||
test("autocomplete: an exhaustive pattern requires the universe, `_` optional", () => {
|
test("autocomplete: an exhaustive pattern requires the universe", () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const name = "getMatcher_fresh";
|
const name = "getMatcher_fresh";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const labels = labelsFor(name, "getMatcher", " /*COMPLETE*/");
|
const labels = labelsFor({
|
||||||
|
name,
|
||||||
|
factory: "getMatcher",
|
||||||
|
body: " /*COMPLETE*/",
|
||||||
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
return labels.then((result) => {
|
return labels.then((result) => {
|
||||||
assert.deepEqual([...result], ["_?", "a", "b", "c"]);
|
assert.deepEqual([...result], ["a", "b", "c"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -414,28 +429,29 @@ test("autocomplete: handled keys drop out of the popup", () => {
|
|||||||
const name = "getMatcher_after_key";
|
const name = "getMatcher_after_key";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const labels = labelsFor(
|
const labels = labelsFor({
|
||||||
name,
|
name,
|
||||||
"getMatcher",
|
factory: "getMatcher",
|
||||||
" a: () => 1,\n /*COMPLETE*/",
|
body: " a: () => 1,\n /*COMPLETE*/",
|
||||||
);
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
return labels.then((result) => {
|
return labels.then((result) => {
|
||||||
assert.deepEqual([...result], ["_?", "b", "c"]);
|
assert.deepEqual([...result], ["b", "c"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("autocomplete: `_` makes the remaining keys optional", () => {
|
test("autocomplete: a fallback makes the remaining keys optional", () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const name = "getMatcher_after_fallback";
|
const name = "getMatcher_with_fallback";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const labels = labelsFor(
|
const labels = labelsFor({
|
||||||
name,
|
name,
|
||||||
"getMatcher",
|
factory: "getMatcher",
|
||||||
" _: () => 0,\n /*COMPLETE*/",
|
body: " /*COMPLETE*/",
|
||||||
);
|
tail: ", () => 0",
|
||||||
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
return labels.then((result) => {
|
return labels.then((result) => {
|
||||||
@@ -443,15 +459,55 @@ test("autocomplete: `_` makes the remaining keys optional", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("autocomplete: with a fallback, handled keys stay optional", () => {
|
||||||
|
// Arrange
|
||||||
|
const name = "getMatcher_with_fallback_after_key";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const labels = labelsFor({
|
||||||
|
name,
|
||||||
|
factory: "getMatcher",
|
||||||
|
body: " a: () => 1,\n /*COMPLETE*/",
|
||||||
|
tail: ", () => 0",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
return labels.then((result) => {
|
||||||
|
assert.deepEqual([...result], ["b?", "c?"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("autocomplete: `getMatcherW` offers the same popup as `getMatcher`", () => {
|
test("autocomplete: `getMatcherW` offers the same popup as `getMatcher`", () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const name = "getMatcherW_fresh";
|
const name = "getMatcherW_fresh";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const labels = labelsFor(name, "getMatcherW", " /*COMPLETE*/");
|
const labels = labelsFor({
|
||||||
|
name,
|
||||||
|
factory: "getMatcherW",
|
||||||
|
body: " /*COMPLETE*/",
|
||||||
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
return labels.then((result) => {
|
return labels.then((result) => {
|
||||||
assert.deepEqual([...result], ["_?", "a", "b", "c"]);
|
assert.deepEqual([...result], ["a", "b", "c"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("autocomplete: `getMatcherW` also offers optional keys with a fallback", () => {
|
||||||
|
// Arrange
|
||||||
|
const name = "getMatcherW_with_fallback";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const labels = labelsFor({
|
||||||
|
name,
|
||||||
|
factory: "getMatcherW",
|
||||||
|
body: " /*COMPLETE*/",
|
||||||
|
tail: ", () => 0",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
return labels.then((result) => {
|
||||||
|
assert.deepEqual([...result], ["a?", "b?", "c?"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+66
-31
@@ -1,4 +1,4 @@
|
|||||||
import type { ValueOf } from "type-fest";
|
import type { Exact, ValueOf } from "type-fest";
|
||||||
|
|
||||||
type UnaryFn<T, R> = (shape: T) => R;
|
type UnaryFn<T, R> = (shape: T) => R;
|
||||||
|
|
||||||
@@ -8,54 +8,89 @@ type PatternReturns<P> = ReturnType<
|
|||||||
Extract<ValueOf<P>, (...args: never[]) => unknown>
|
Extract<ValueOf<P>, (...args: never[]) => unknown>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type Handlers<R, T extends string | number> = { [K in T]: UnaryFn<K, R> };
|
type Handlers<T extends string | number, R> = { [K in T]: UnaryFn<K, R> };
|
||||||
|
|
||||||
// Fallback form: `_` is a required key, the T-keys are optional.
|
// The fallback is a *second argument*, not a property of the handler map,
|
||||||
type Fallback<R, T extends string | number> = Partial<Handlers<R, T>> & {
|
// because its parameter is the remainder `Exclude<T, keyof Handled>` and TypeScript
|
||||||
_: UnaryFn<T, R>;
|
// fixes a property's contextual type before it infers its sibling keys. A later
|
||||||
};
|
// argument, by contrast, is contextually typed from inference on an earlier
|
||||||
|
// one, so the split is what makes the remainder expressible at all.
|
||||||
|
// See development/library.md.
|
||||||
|
type Fallback<T extends string | number, Handled, R> = UnaryFn<
|
||||||
|
Exclude<T, keyof Handled>,
|
||||||
|
R
|
||||||
|
>;
|
||||||
|
|
||||||
// Completion form: T-keys required, `_` optional. Overload #1, because
|
// A fallback is redundant once the handler map covers `T`. The guard is folded
|
||||||
// TypeScript takes the *first* overload as the contextual type for the popup.
|
// into `Handled`'s own (self-referential) constraint so it is checked *after*
|
||||||
type ExhaustiveLoose<R, T extends string | number> = Handlers<R, T> & {
|
// inference; a conditional in the fallback's parameter type is evaluated while
|
||||||
_?: UnaryFn<T, R>;
|
// `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`.
|
||||||
|
|
||||||
// oxlint-disable typescript/unified-signatures
|
// oxlint-disable typescript/unified-signatures
|
||||||
// Strict returns: one common `R`. Overload order is load-bearing:
|
// Strict returns: one common `R`. Overload order is load-bearing:
|
||||||
// #1 ExhaustiveLoose -> autocomplete `_?, a, b`
|
// #1 Handlers (first) -> the exhaustive form and the autocomplete popup
|
||||||
// #2 Fallback -> accepts a partial pattern
|
// #2 Fallback (last) -> accepts a partial handler map plus a fallback
|
||||||
// #3 Handlers (last) -> "Property 'b' is missing" is the reported error
|
|
||||||
interface MatcherStrict<T extends string | number> {
|
interface MatcherStrict<T extends string | number> {
|
||||||
<R>(pattern: ExhaustiveLoose<R, T>): UnaryFn<T, R>;
|
<R>(handlers: Handlers<T, R>): UnaryFn<T, R>;
|
||||||
<R>(pattern: Fallback<R, T>): UnaryFn<T, R>;
|
<
|
||||||
<R>(pattern: Handlers<R, T>): UnaryFn<T, R>;
|
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>;
|
||||||
}
|
}
|
||||||
// oxlint-enable typescript/unified-signatures
|
// oxlint-enable typescript/unified-signatures
|
||||||
|
|
||||||
// Widened returns: the union of every handler's return type. `P` is inferred
|
// Widened returns: the union of every handler's return type. `P` is inferred
|
||||||
// from the whole parameter, whose closed constraint supplies the
|
// from the whole handler map, whose closed constraint supplies the
|
||||||
// contextual/autocomplete type; the `keyof P` guard appended to each overload
|
// contextual/autocomplete type.
|
||||||
// rejects keys outside `T`/`_`.
|
// oxlint-disable typescript/unified-signatures
|
||||||
interface MatcherWidening<T extends string | number> {
|
interface MatcherWidening<T extends string | number> {
|
||||||
<P extends ExhaustiveLoose<unknown, T>>(
|
<P extends Exact<Handlers<T, unknown>, P>>(
|
||||||
pattern: P & (keyof P extends T | "_" ? unknown : never),
|
handlers: P,
|
||||||
): UnaryFn<T, PatternReturns<P>>;
|
|
||||||
<P extends Fallback<unknown, T>>(
|
|
||||||
pattern: P & (keyof P extends T | "_" ? unknown : never),
|
|
||||||
): UnaryFn<T, PatternReturns<P>>;
|
|
||||||
<P extends Handlers<unknown, T>>(
|
|
||||||
pattern: P & (keyof P extends T | "_" ? unknown : never),
|
|
||||||
): UnaryFn<T, PatternReturns<P>>;
|
): UnaryFn<T, PatternReturns<P>>;
|
||||||
|
<
|
||||||
|
R,
|
||||||
|
Handled extends Exact<Partial<Handlers<T, unknown>>, Handled> &
|
||||||
|
MustBePartial<T, Handled>,
|
||||||
|
>(
|
||||||
|
handlers: Handled,
|
||||||
|
fallback: Fallback<T, Handled, R>,
|
||||||
|
): UnaryFn<T, PatternReturns<Handled> | R>;
|
||||||
}
|
}
|
||||||
|
// oxlint-enable typescript/unified-signatures
|
||||||
|
|
||||||
type HandlerMap = Record<string | number, UnaryFn<never, unknown> | undefined>;
|
type HandlerMap = Record<string | number, UnaryFn<never, unknown> | undefined>;
|
||||||
|
|
||||||
const dispatch =
|
const dispatch =
|
||||||
(pattern: HandlerMap) =>
|
(handlers: HandlerMap, fallback?: UnaryFn<never, unknown>) =>
|
||||||
(shape: string | number): unknown =>
|
(shape: string | number): unknown =>
|
||||||
// oxlint-disable-next-line typescript/no-non-null-assertion typescript/no-unsafe-type-assertion
|
(
|
||||||
(pattern[shape] ?? pattern["_"]!)(shape as never);
|
handlers[shape] ??
|
||||||
|
fallback ??
|
||||||
|
(() => {
|
||||||
|
throw new Error(`Unhandled shape: ${shape}`);
|
||||||
|
})
|
||||||
|
)(
|
||||||
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
||||||
|
shape as never,
|
||||||
|
);
|
||||||
|
|
||||||
export const getMatcher = <T extends string | number>(): MatcherStrict<T> =>
|
export const getMatcher = <T extends string | number>(): MatcherStrict<T> =>
|
||||||
dispatch;
|
dispatch;
|
||||||
|
|||||||
Reference in new issue
Block a user