@onrails/pattern 0.3.2 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assert.d.cts +24 -0
- package/dist/assert.d.ts +24 -0
- package/dist/exhaustive.d.cts +23 -0
- package/dist/exhaustive.d.ts +23 -0
- package/dist/index.cjs +10 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -329
- package/dist/index.d.ts +6 -329
- package/dist/index.js +10 -12
- package/dist/index.js.map +1 -1
- package/dist/match.d.cts +247 -0
- package/dist/match.d.ts +247 -0
- package/dist/narrow.d.cts +7 -0
- package/dist/narrow.d.ts +7 -0
- package/dist/tag.d.cts +2 -3
- package/dist/tag.d.ts +2 -3
- package/dist/when.d.cts +26 -0
- package/dist/when.d.ts +26 -0
- package/package.json +19 -9
package/dist/match.d.cts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type { ExhaustiveResult, ExhaustMatched } from "./exhaustive.js";
|
|
2
|
+
import type { Narrow, NarrowUnion } from "./narrow.js";
|
|
3
|
+
/**
|
|
4
|
+
* A pattern that matches a value of type `T`. One of:
|
|
5
|
+
*
|
|
6
|
+
* - **Literal** — a primitive (`"a"`, `42`, `true`) matched by `===`.
|
|
7
|
+
* - **Object pattern** — a `Partial<T>` of shallow key/value pairs that
|
|
8
|
+
* must all match by strict equality. Only available when `T` is an
|
|
9
|
+
* object type.
|
|
10
|
+
* - **Guard** — a function `(input: T) => boolean`. Type predicates
|
|
11
|
+
* (`(x): x is U`) narrow the handler input via {@link Narrow}.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* type Event = { kind: "click"; x: number; y: number } | { kind: "key"; code: string };
|
|
16
|
+
*
|
|
17
|
+
* const p1: Pattern<Event> = { kind: "click" }; // object pattern
|
|
18
|
+
* const p2: Pattern<Event> = (e) => e.kind === "key"; // guard
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export type Pattern<T> = T | ObjectPattern<T> | PatternGuard<T>;
|
|
22
|
+
type ObjectPattern<T> = T extends object ? Partial<T> : never;
|
|
23
|
+
type PatternGuard<T> = (input: T) => boolean;
|
|
24
|
+
type Case<T, R> = {
|
|
25
|
+
readonly test: (input: T) => boolean;
|
|
26
|
+
readonly run: (input: T) => R;
|
|
27
|
+
};
|
|
28
|
+
declare const NO_INPUT: unique symbol;
|
|
29
|
+
type NoInput = typeof NO_INPUT;
|
|
30
|
+
type NextResult<Locked extends boolean, R, R2> = Locked extends true ? R : R | R2;
|
|
31
|
+
type HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2;
|
|
32
|
+
/**
|
|
33
|
+
* Fluent builder for {@link match}. Tracks the handled cases at the type
|
|
34
|
+
* level so `.exhaustive()` only compiles when every union member is covered.
|
|
35
|
+
*
|
|
36
|
+
* The `Locked` phantom flag (set via `.returnType<R>()`) constrains every
|
|
37
|
+
* subsequent handler to return `R`, useful when branch return-type
|
|
38
|
+
* inference would widen to a union narrower than the target slot
|
|
39
|
+
* (e.g. `ReactNode`, an API DTO).
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* const describe = match<Event>()
|
|
44
|
+
* .with({ kind: "click" }, (e) => `click @ ${e.x},${e.y}`)
|
|
45
|
+
* .with({ kind: "key" }, (e) => `key ${e.code}`)
|
|
46
|
+
* .exhaustive();
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handled extends readonly unknown[] = [], Locked extends boolean = false> {
|
|
50
|
+
private readonly cases;
|
|
51
|
+
private readonly input;
|
|
52
|
+
/** Phantom tuple of handled cases — only its compile-time contents matter. */
|
|
53
|
+
readonly _handled: Handled;
|
|
54
|
+
/**
|
|
55
|
+
* Construct a builder directly. Prefer the {@link match} factory — the
|
|
56
|
+
* constructor exists so internal combinators can thread accumulated `cases`,
|
|
57
|
+
* the optional captured `input`, and the phantom `_handled` tuple.
|
|
58
|
+
*/
|
|
59
|
+
constructor(cases: readonly Case<T, unknown>[], input: T | NoInput,
|
|
60
|
+
/** Phantom tuple of handled cases — only its compile-time contents matter. */
|
|
61
|
+
_handled?: Handled);
|
|
62
|
+
/** Appends a runtime case plus a phantom `H` entry on the handled tuple. */
|
|
63
|
+
private addCase;
|
|
64
|
+
/**
|
|
65
|
+
* Add a case: when `input` satisfies `pattern`, run `handler` on the narrowed
|
|
66
|
+
* value. The pattern may be a literal, a shallow object pattern, or a guard
|
|
67
|
+
* ({@link Pattern}); a type-predicate guard narrows the handler input. Each
|
|
68
|
+
* `.with` advances compile-time exhaustiveness tracking so `.exhaustive()`
|
|
69
|
+
* knows which union members remain.
|
|
70
|
+
*
|
|
71
|
+
* @param pattern - literal, object pattern, or guard to test against
|
|
72
|
+
* @param handler - run on the narrowed input when the pattern matches
|
|
73
|
+
* @returns a builder with this case appended
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* type Event =
|
|
78
|
+
* | { type: "message"; content: string }
|
|
79
|
+
* | { type: "error"; message: string }
|
|
80
|
+
* | { type: "done" };
|
|
81
|
+
*
|
|
82
|
+
* const summary = match({ type: "message", content: "hi" } as Event)
|
|
83
|
+
* .with({ type: "message" }, (e) => e.content) // e: { type: "message"; content: string }
|
|
84
|
+
* .with({ type: "error" }, (e) => e.message)
|
|
85
|
+
* .with({ type: "done" }, () => "—")
|
|
86
|
+
* .exhaustive(); // "hi"
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
with<const P extends Pattern<T>, R2>(pattern: P, handler: (input: Narrow<T, P>) => HandlerReturn<Locked, R, R2>): MatchBuilder<T, NextResult<Locked, R, R2>, HasInput, readonly [...Handled, ExhaustMatched<T, P>], Locked>;
|
|
90
|
+
/**
|
|
91
|
+
* One handler for several patterns (OR). The handler input is the union of
|
|
92
|
+
* the members narrowed by each pattern, and all of them are marked handled
|
|
93
|
+
* for exhaustiveness.
|
|
94
|
+
*
|
|
95
|
+
* @param patterns - patterns to test; any match runs `handler`
|
|
96
|
+
* @param handler - run on the union of narrowed members
|
|
97
|
+
* @returns a builder with the shared case appended
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* type Job =
|
|
102
|
+
* | { kind: "queued"; id: string }
|
|
103
|
+
* | { kind: "running"; id: string }
|
|
104
|
+
* | { kind: "done"; ok: boolean };
|
|
105
|
+
*
|
|
106
|
+
* const status = match({ kind: "running", id: "j1" } as Job)
|
|
107
|
+
* .withOneOf([{ kind: "queued" }, { kind: "running" }], (j) => j.id) // j: queued | running
|
|
108
|
+
* .with({ kind: "done" }, (j) => (j.ok ? "yes" : "no"))
|
|
109
|
+
* .exhaustive(); // "j1"
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
withOneOf<const Ps extends readonly Pattern<T>[], R2>(patterns: Ps, handler: (input: NarrowUnion<T, Ps>) => HandlerReturn<Locked, R, R2>): MatchBuilder<T, NextResult<Locked, R, R2>, HasInput, readonly [...Handled, NarrowUnion<T, Ps>], Locked>;
|
|
113
|
+
/**
|
|
114
|
+
* {@link withOneOf} for exactly two patterns — sugar that avoids the array.
|
|
115
|
+
*
|
|
116
|
+
* @param pattern1 - first pattern
|
|
117
|
+
* @param pattern2 - second pattern
|
|
118
|
+
* @param handler - run on the union of the two narrowed members
|
|
119
|
+
* @returns a builder with the shared case appended
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* type Provider = "ollama" | "openrouter";
|
|
124
|
+
*
|
|
125
|
+
* const pick = match<Provider>()
|
|
126
|
+
* .withEither("ollama", "openrouter", (p) => p) // p: "ollama" | "openrouter"
|
|
127
|
+
* .exhaustive();
|
|
128
|
+
* pick("ollama"); // "ollama"
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
withEither<const P1 extends Pattern<T>, const P2 extends Pattern<T>, R2>(pattern1: P1, pattern2: P2, handler: (input: NarrowUnion<T, readonly [P1, P2]>) => HandlerReturn<Locked, R, R2>): MatchBuilder<T, NextResult<Locked, R, R2>, HasInput, readonly [...Handled, NarrowUnion<T, readonly [P1, P2]>], Locked>;
|
|
132
|
+
/**
|
|
133
|
+
* Lock the result type. All subsequent `.with()` handlers must return `R2`.
|
|
134
|
+
* Useful when branch return-type inference widens to a union narrower than
|
|
135
|
+
* the slot the match feeds into (e.g. `ReactNode`).
|
|
136
|
+
*
|
|
137
|
+
* Also the fix for literal-widening: when a handler returns an object whose
|
|
138
|
+
* field is a union literal, inference widens it (e.g. `{ mode: "native" }`
|
|
139
|
+
* becomes `{ mode: string }`, breaking a `Mode` union). Lock the target type
|
|
140
|
+
* instead of sprinkling `as const`:
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* type State = { mode: "compat" | "native" };
|
|
145
|
+
* const next = match(flag)
|
|
146
|
+
* .returnType<State>() // handlers now checked against State
|
|
147
|
+
* .with("--native", () => ({ mode: "native" })) // no `as const` needed
|
|
148
|
+
* .otherwise(() => state);
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true>;
|
|
152
|
+
/**
|
|
153
|
+
* Run the accumulated cases now, without a compile-time exhaustiveness check.
|
|
154
|
+
* Uses the value passed to {@link match} (data-first), or the value passed to
|
|
155
|
+
* `run` (curried). Throws `"Non-exhaustive match"` if no case matches at
|
|
156
|
+
* runtime — prefer {@link exhaustive} when the input is a closed union, or
|
|
157
|
+
* {@link otherwise} for an explicit fallback.
|
|
158
|
+
*
|
|
159
|
+
* @param input - the value to match when the builder was created curried
|
|
160
|
+
* @returns the matched handler's result
|
|
161
|
+
* @throws if no case matches the input
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```ts
|
|
165
|
+
* type Event = { type: "error"; message: string } | { type: "done" };
|
|
166
|
+
*
|
|
167
|
+
* const msg = match({ type: "error", message: "x" } as Event)
|
|
168
|
+
* .with({ type: "error" }, (e) => e.message)
|
|
169
|
+
* .run({ type: "error", message: "net" }); // "net"
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
run(...input: readonly [input: T] | readonly []): R;
|
|
173
|
+
/**
|
|
174
|
+
* Settle the match with a compile-time exhaustiveness guarantee. Only
|
|
175
|
+
* type-checks when every member of `T` has been handled; otherwise the return
|
|
176
|
+
* type becomes {@link NonExhaustiveError}, surfacing the missing cases as a
|
|
177
|
+
* type error. Returns the result directly when data-first ({@link match}
|
|
178
|
+
* called with a value), or a reusable `(input: T) => R` matcher when curried.
|
|
179
|
+
*
|
|
180
|
+
* @returns the result (data-first) or a matcher function (curried)
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* type Event =
|
|
185
|
+
* | { type: "message"; content: string }
|
|
186
|
+
* | { type: "error"; message: string }
|
|
187
|
+
* | { type: "done" };
|
|
188
|
+
*
|
|
189
|
+
* // Curried — reusable matcher; omitting a `.with` would be a type error.
|
|
190
|
+
* const describe = match<Event>()
|
|
191
|
+
* .with({ type: "message" }, (e) => e.content)
|
|
192
|
+
* .with({ type: "error" }, (e) => `! ${e.message}`)
|
|
193
|
+
* .with({ type: "done" }, () => "done")
|
|
194
|
+
* .exhaustive();
|
|
195
|
+
* describe({ type: "done" }); // "done"
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
exhaustive(): ExhaustiveResult<T, Handled, R, HasInput>;
|
|
199
|
+
/**
|
|
200
|
+
* Settle the match with a catch-all fallback, dropping the exhaustiveness
|
|
201
|
+
* requirement — `handler` runs (on the full input type) when no case matched.
|
|
202
|
+
* Returns the result directly when data-first, or a reusable matcher when
|
|
203
|
+
* curried.
|
|
204
|
+
*
|
|
205
|
+
* @param handler - fallback run on the input when no prior case matched
|
|
206
|
+
* @returns the result (data-first) or a matcher function (curried)
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```ts
|
|
210
|
+
* type Event =
|
|
211
|
+
* | { type: "message"; content: string }
|
|
212
|
+
* | { type: "error"; message: string }
|
|
213
|
+
* | { type: "done" };
|
|
214
|
+
*
|
|
215
|
+
* const len = match({ type: "done" } as Event)
|
|
216
|
+
* .with({ type: "message" }, (e) => e.content.length)
|
|
217
|
+
* .otherwise(() => -1); // -1
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Start a pattern-matching expression. Two call shapes:
|
|
224
|
+
*
|
|
225
|
+
* - `match(value)` — data-first; subsequent `.exhaustive()` or
|
|
226
|
+
* `.otherwise(fn)` runs immediately against `value`.
|
|
227
|
+
* - `match<T>()` — curried; subsequent `.exhaustive()` returns a function
|
|
228
|
+
* `(value: T) => R` for use in `pipe(...)` or as a reusable matcher.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```ts
|
|
232
|
+
* // Data-first
|
|
233
|
+
* const out = match({ kind: "click", x: 1, y: 2 } as Event)
|
|
234
|
+
* .with({ kind: "click" }, (e) => e.x + e.y)
|
|
235
|
+
* .with({ kind: "key" }, () => -1)
|
|
236
|
+
* .exhaustive();
|
|
237
|
+
*
|
|
238
|
+
* // Curried — build a reusable matcher
|
|
239
|
+
* const describe = match<Event>()
|
|
240
|
+
* .with({ kind: "click" }, (e) => `click @ ${e.x},${e.y}`)
|
|
241
|
+
* .with({ kind: "key" }, (e) => `key ${e.code}`)
|
|
242
|
+
* .exhaustive();
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
export declare function match<T>(input: T): MatchBuilder<T, never, true>;
|
|
246
|
+
export declare function match<T = never>(): MatchBuilder<T, never, false>;
|
|
247
|
+
export {};
|
package/dist/match.d.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type { ExhaustiveResult, ExhaustMatched } from "./exhaustive.js";
|
|
2
|
+
import type { Narrow, NarrowUnion } from "./narrow.js";
|
|
3
|
+
/**
|
|
4
|
+
* A pattern that matches a value of type `T`. One of:
|
|
5
|
+
*
|
|
6
|
+
* - **Literal** — a primitive (`"a"`, `42`, `true`) matched by `===`.
|
|
7
|
+
* - **Object pattern** — a `Partial<T>` of shallow key/value pairs that
|
|
8
|
+
* must all match by strict equality. Only available when `T` is an
|
|
9
|
+
* object type.
|
|
10
|
+
* - **Guard** — a function `(input: T) => boolean`. Type predicates
|
|
11
|
+
* (`(x): x is U`) narrow the handler input via {@link Narrow}.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* type Event = { kind: "click"; x: number; y: number } | { kind: "key"; code: string };
|
|
16
|
+
*
|
|
17
|
+
* const p1: Pattern<Event> = { kind: "click" }; // object pattern
|
|
18
|
+
* const p2: Pattern<Event> = (e) => e.kind === "key"; // guard
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export type Pattern<T> = T | ObjectPattern<T> | PatternGuard<T>;
|
|
22
|
+
type ObjectPattern<T> = T extends object ? Partial<T> : never;
|
|
23
|
+
type PatternGuard<T> = (input: T) => boolean;
|
|
24
|
+
type Case<T, R> = {
|
|
25
|
+
readonly test: (input: T) => boolean;
|
|
26
|
+
readonly run: (input: T) => R;
|
|
27
|
+
};
|
|
28
|
+
declare const NO_INPUT: unique symbol;
|
|
29
|
+
type NoInput = typeof NO_INPUT;
|
|
30
|
+
type NextResult<Locked extends boolean, R, R2> = Locked extends true ? R : R | R2;
|
|
31
|
+
type HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2;
|
|
32
|
+
/**
|
|
33
|
+
* Fluent builder for {@link match}. Tracks the handled cases at the type
|
|
34
|
+
* level so `.exhaustive()` only compiles when every union member is covered.
|
|
35
|
+
*
|
|
36
|
+
* The `Locked` phantom flag (set via `.returnType<R>()`) constrains every
|
|
37
|
+
* subsequent handler to return `R`, useful when branch return-type
|
|
38
|
+
* inference would widen to a union narrower than the target slot
|
|
39
|
+
* (e.g. `ReactNode`, an API DTO).
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* const describe = match<Event>()
|
|
44
|
+
* .with({ kind: "click" }, (e) => `click @ ${e.x},${e.y}`)
|
|
45
|
+
* .with({ kind: "key" }, (e) => `key ${e.code}`)
|
|
46
|
+
* .exhaustive();
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handled extends readonly unknown[] = [], Locked extends boolean = false> {
|
|
50
|
+
private readonly cases;
|
|
51
|
+
private readonly input;
|
|
52
|
+
/** Phantom tuple of handled cases — only its compile-time contents matter. */
|
|
53
|
+
readonly _handled: Handled;
|
|
54
|
+
/**
|
|
55
|
+
* Construct a builder directly. Prefer the {@link match} factory — the
|
|
56
|
+
* constructor exists so internal combinators can thread accumulated `cases`,
|
|
57
|
+
* the optional captured `input`, and the phantom `_handled` tuple.
|
|
58
|
+
*/
|
|
59
|
+
constructor(cases: readonly Case<T, unknown>[], input: T | NoInput,
|
|
60
|
+
/** Phantom tuple of handled cases — only its compile-time contents matter. */
|
|
61
|
+
_handled?: Handled);
|
|
62
|
+
/** Appends a runtime case plus a phantom `H` entry on the handled tuple. */
|
|
63
|
+
private addCase;
|
|
64
|
+
/**
|
|
65
|
+
* Add a case: when `input` satisfies `pattern`, run `handler` on the narrowed
|
|
66
|
+
* value. The pattern may be a literal, a shallow object pattern, or a guard
|
|
67
|
+
* ({@link Pattern}); a type-predicate guard narrows the handler input. Each
|
|
68
|
+
* `.with` advances compile-time exhaustiveness tracking so `.exhaustive()`
|
|
69
|
+
* knows which union members remain.
|
|
70
|
+
*
|
|
71
|
+
* @param pattern - literal, object pattern, or guard to test against
|
|
72
|
+
* @param handler - run on the narrowed input when the pattern matches
|
|
73
|
+
* @returns a builder with this case appended
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* type Event =
|
|
78
|
+
* | { type: "message"; content: string }
|
|
79
|
+
* | { type: "error"; message: string }
|
|
80
|
+
* | { type: "done" };
|
|
81
|
+
*
|
|
82
|
+
* const summary = match({ type: "message", content: "hi" } as Event)
|
|
83
|
+
* .with({ type: "message" }, (e) => e.content) // e: { type: "message"; content: string }
|
|
84
|
+
* .with({ type: "error" }, (e) => e.message)
|
|
85
|
+
* .with({ type: "done" }, () => "—")
|
|
86
|
+
* .exhaustive(); // "hi"
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
with<const P extends Pattern<T>, R2>(pattern: P, handler: (input: Narrow<T, P>) => HandlerReturn<Locked, R, R2>): MatchBuilder<T, NextResult<Locked, R, R2>, HasInput, readonly [...Handled, ExhaustMatched<T, P>], Locked>;
|
|
90
|
+
/**
|
|
91
|
+
* One handler for several patterns (OR). The handler input is the union of
|
|
92
|
+
* the members narrowed by each pattern, and all of them are marked handled
|
|
93
|
+
* for exhaustiveness.
|
|
94
|
+
*
|
|
95
|
+
* @param patterns - patterns to test; any match runs `handler`
|
|
96
|
+
* @param handler - run on the union of narrowed members
|
|
97
|
+
* @returns a builder with the shared case appended
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* type Job =
|
|
102
|
+
* | { kind: "queued"; id: string }
|
|
103
|
+
* | { kind: "running"; id: string }
|
|
104
|
+
* | { kind: "done"; ok: boolean };
|
|
105
|
+
*
|
|
106
|
+
* const status = match({ kind: "running", id: "j1" } as Job)
|
|
107
|
+
* .withOneOf([{ kind: "queued" }, { kind: "running" }], (j) => j.id) // j: queued | running
|
|
108
|
+
* .with({ kind: "done" }, (j) => (j.ok ? "yes" : "no"))
|
|
109
|
+
* .exhaustive(); // "j1"
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
withOneOf<const Ps extends readonly Pattern<T>[], R2>(patterns: Ps, handler: (input: NarrowUnion<T, Ps>) => HandlerReturn<Locked, R, R2>): MatchBuilder<T, NextResult<Locked, R, R2>, HasInput, readonly [...Handled, NarrowUnion<T, Ps>], Locked>;
|
|
113
|
+
/**
|
|
114
|
+
* {@link withOneOf} for exactly two patterns — sugar that avoids the array.
|
|
115
|
+
*
|
|
116
|
+
* @param pattern1 - first pattern
|
|
117
|
+
* @param pattern2 - second pattern
|
|
118
|
+
* @param handler - run on the union of the two narrowed members
|
|
119
|
+
* @returns a builder with the shared case appended
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* type Provider = "ollama" | "openrouter";
|
|
124
|
+
*
|
|
125
|
+
* const pick = match<Provider>()
|
|
126
|
+
* .withEither("ollama", "openrouter", (p) => p) // p: "ollama" | "openrouter"
|
|
127
|
+
* .exhaustive();
|
|
128
|
+
* pick("ollama"); // "ollama"
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
withEither<const P1 extends Pattern<T>, const P2 extends Pattern<T>, R2>(pattern1: P1, pattern2: P2, handler: (input: NarrowUnion<T, readonly [P1, P2]>) => HandlerReturn<Locked, R, R2>): MatchBuilder<T, NextResult<Locked, R, R2>, HasInput, readonly [...Handled, NarrowUnion<T, readonly [P1, P2]>], Locked>;
|
|
132
|
+
/**
|
|
133
|
+
* Lock the result type. All subsequent `.with()` handlers must return `R2`.
|
|
134
|
+
* Useful when branch return-type inference widens to a union narrower than
|
|
135
|
+
* the slot the match feeds into (e.g. `ReactNode`).
|
|
136
|
+
*
|
|
137
|
+
* Also the fix for literal-widening: when a handler returns an object whose
|
|
138
|
+
* field is a union literal, inference widens it (e.g. `{ mode: "native" }`
|
|
139
|
+
* becomes `{ mode: string }`, breaking a `Mode` union). Lock the target type
|
|
140
|
+
* instead of sprinkling `as const`:
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* type State = { mode: "compat" | "native" };
|
|
145
|
+
* const next = match(flag)
|
|
146
|
+
* .returnType<State>() // handlers now checked against State
|
|
147
|
+
* .with("--native", () => ({ mode: "native" })) // no `as const` needed
|
|
148
|
+
* .otherwise(() => state);
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true>;
|
|
152
|
+
/**
|
|
153
|
+
* Run the accumulated cases now, without a compile-time exhaustiveness check.
|
|
154
|
+
* Uses the value passed to {@link match} (data-first), or the value passed to
|
|
155
|
+
* `run` (curried). Throws `"Non-exhaustive match"` if no case matches at
|
|
156
|
+
* runtime — prefer {@link exhaustive} when the input is a closed union, or
|
|
157
|
+
* {@link otherwise} for an explicit fallback.
|
|
158
|
+
*
|
|
159
|
+
* @param input - the value to match when the builder was created curried
|
|
160
|
+
* @returns the matched handler's result
|
|
161
|
+
* @throws if no case matches the input
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```ts
|
|
165
|
+
* type Event = { type: "error"; message: string } | { type: "done" };
|
|
166
|
+
*
|
|
167
|
+
* const msg = match({ type: "error", message: "x" } as Event)
|
|
168
|
+
* .with({ type: "error" }, (e) => e.message)
|
|
169
|
+
* .run({ type: "error", message: "net" }); // "net"
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
run(...input: readonly [input: T] | readonly []): R;
|
|
173
|
+
/**
|
|
174
|
+
* Settle the match with a compile-time exhaustiveness guarantee. Only
|
|
175
|
+
* type-checks when every member of `T` has been handled; otherwise the return
|
|
176
|
+
* type becomes {@link NonExhaustiveError}, surfacing the missing cases as a
|
|
177
|
+
* type error. Returns the result directly when data-first ({@link match}
|
|
178
|
+
* called with a value), or a reusable `(input: T) => R` matcher when curried.
|
|
179
|
+
*
|
|
180
|
+
* @returns the result (data-first) or a matcher function (curried)
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* type Event =
|
|
185
|
+
* | { type: "message"; content: string }
|
|
186
|
+
* | { type: "error"; message: string }
|
|
187
|
+
* | { type: "done" };
|
|
188
|
+
*
|
|
189
|
+
* // Curried — reusable matcher; omitting a `.with` would be a type error.
|
|
190
|
+
* const describe = match<Event>()
|
|
191
|
+
* .with({ type: "message" }, (e) => e.content)
|
|
192
|
+
* .with({ type: "error" }, (e) => `! ${e.message}`)
|
|
193
|
+
* .with({ type: "done" }, () => "done")
|
|
194
|
+
* .exhaustive();
|
|
195
|
+
* describe({ type: "done" }); // "done"
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
exhaustive(): ExhaustiveResult<T, Handled, R, HasInput>;
|
|
199
|
+
/**
|
|
200
|
+
* Settle the match with a catch-all fallback, dropping the exhaustiveness
|
|
201
|
+
* requirement — `handler` runs (on the full input type) when no case matched.
|
|
202
|
+
* Returns the result directly when data-first, or a reusable matcher when
|
|
203
|
+
* curried.
|
|
204
|
+
*
|
|
205
|
+
* @param handler - fallback run on the input when no prior case matched
|
|
206
|
+
* @returns the result (data-first) or a matcher function (curried)
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```ts
|
|
210
|
+
* type Event =
|
|
211
|
+
* | { type: "message"; content: string }
|
|
212
|
+
* | { type: "error"; message: string }
|
|
213
|
+
* | { type: "done" };
|
|
214
|
+
*
|
|
215
|
+
* const len = match({ type: "done" } as Event)
|
|
216
|
+
* .with({ type: "message" }, (e) => e.content.length)
|
|
217
|
+
* .otherwise(() => -1); // -1
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Start a pattern-matching expression. Two call shapes:
|
|
224
|
+
*
|
|
225
|
+
* - `match(value)` — data-first; subsequent `.exhaustive()` or
|
|
226
|
+
* `.otherwise(fn)` runs immediately against `value`.
|
|
227
|
+
* - `match<T>()` — curried; subsequent `.exhaustive()` returns a function
|
|
228
|
+
* `(value: T) => R` for use in `pipe(...)` or as a reusable matcher.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```ts
|
|
232
|
+
* // Data-first
|
|
233
|
+
* const out = match({ kind: "click", x: 1, y: 2 } as Event)
|
|
234
|
+
* .with({ kind: "click" }, (e) => e.x + e.y)
|
|
235
|
+
* .with({ kind: "key" }, () => -1)
|
|
236
|
+
* .exhaustive();
|
|
237
|
+
*
|
|
238
|
+
* // Curried — build a reusable matcher
|
|
239
|
+
* const describe = match<Event>()
|
|
240
|
+
* .with({ kind: "click" }, (e) => `click @ ${e.x},${e.y}`)
|
|
241
|
+
* .with({ kind: "key" }, (e) => `key ${e.code}`)
|
|
242
|
+
* .exhaustive();
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
export declare function match<T>(input: T): MatchBuilder<T, never, true>;
|
|
246
|
+
export declare function match<T = never>(): MatchBuilder<T, never, false>;
|
|
247
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type GuardTarget<F> = F extends (input: any) => input is infer U ? U : never;
|
|
2
|
+
type NarrowObject<T, P> = [Extract<T, P>] extends [never] ? T & P : Extract<T, P>;
|
|
3
|
+
/** Union of members narrowed by each pattern in `Ps` (multi-branch handlers). */
|
|
4
|
+
export type NarrowUnion<T, Ps extends readonly unknown[]> = Narrow<T, Ps[number]>;
|
|
5
|
+
/** Narrows `input` when `pattern` is a shallow object, literal discriminant, or type predicate. */
|
|
6
|
+
export type Narrow<T, P> = P extends (input: T) => boolean ? [GuardTarget<P>] extends [never] ? T : NarrowObject<T, GuardTarget<P>> : P extends Record<string, unknown> ? NarrowObject<T, P> : T extends P ? Extract<T, P> : T;
|
|
7
|
+
export {};
|
package/dist/narrow.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type GuardTarget<F> = F extends (input: any) => input is infer U ? U : never;
|
|
2
|
+
type NarrowObject<T, P> = [Extract<T, P>] extends [never] ? T & P : Extract<T, P>;
|
|
3
|
+
/** Union of members narrowed by each pattern in `Ps` (multi-branch handlers). */
|
|
4
|
+
export type NarrowUnion<T, Ps extends readonly unknown[]> = Narrow<T, Ps[number]>;
|
|
5
|
+
/** Narrows `input` when `pattern` is a shallow object, literal discriminant, or type predicate. */
|
|
6
|
+
export type Narrow<T, P> = P extends (input: T) => boolean ? [GuardTarget<P>] extends [never] ? T : NarrowObject<T, GuardTarget<P>> : P extends Record<string, unknown> ? NarrowObject<T, P> : T extends P ? Extract<T, P> : T;
|
|
7
|
+
export {};
|
package/dist/tag.d.cts
CHANGED
|
@@ -28,6 +28,5 @@ type TagHandlers<T extends Tagged, R> = {
|
|
|
28
28
|
* label(err("x")); // "err:x"
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
31
|
-
declare const matchTag: <T extends Tagged, R>(value: T, handlers: TagHandlers<T, R>) => R;
|
|
32
|
-
|
|
33
|
-
export { matchTag };
|
|
31
|
+
export declare const matchTag: <T extends Tagged, R>(value: T, handlers: TagHandlers<T, R>) => R;
|
|
32
|
+
export {};
|
package/dist/tag.d.ts
CHANGED
|
@@ -28,6 +28,5 @@ type TagHandlers<T extends Tagged, R> = {
|
|
|
28
28
|
* label(err("x")); // "err:x"
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
31
|
-
declare const matchTag: <T extends Tagged, R>(value: T, handlers: TagHandlers<T, R>) => R;
|
|
32
|
-
|
|
33
|
-
export { matchTag };
|
|
31
|
+
export declare const matchTag: <T extends Tagged, R>(value: T, handlers: TagHandlers<T, R>) => R;
|
|
32
|
+
export {};
|
package/dist/when.d.cts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Pattern } from "./match.js";
|
|
2
|
+
/**
|
|
3
|
+
* Guard pattern for {@link match}.with.
|
|
4
|
+
*
|
|
5
|
+
* Accepts plain boolean guards (`(x) => x.length > 0`) or TS type-predicate
|
|
6
|
+
* guards (`(x): x is Foo => ...`). For type predicates, the matched handler's
|
|
7
|
+
* input is narrowed to the predicate's target type via {@link Narrow}.
|
|
8
|
+
*
|
|
9
|
+
* Note: a plain boolean guard does not advance exhaustiveness tracking — pair
|
|
10
|
+
* it with `.otherwise()`, or use a type predicate to narrow the union.
|
|
11
|
+
*
|
|
12
|
+
* @param guard - boolean or type-predicate guard function
|
|
13
|
+
* @returns the guard, usable as a {@link Pattern} in `.with`
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const sign = match<number>()
|
|
18
|
+
* .with(when((n: number) => n < 0), () => "neg")
|
|
19
|
+
* .with(when((n: number) => n >= 0), () => "non-neg")
|
|
20
|
+
* .exhaustive();
|
|
21
|
+
* sign(-1); // "neg"
|
|
22
|
+
* sign(0); // "non-neg"
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function when<T, U extends T>(guard: (input: T) => input is U): (input: T) => input is U;
|
|
26
|
+
export declare function when<T>(guard: (input: T) => boolean): Pattern<T>;
|
package/dist/when.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Pattern } from "./match.js";
|
|
2
|
+
/**
|
|
3
|
+
* Guard pattern for {@link match}.with.
|
|
4
|
+
*
|
|
5
|
+
* Accepts plain boolean guards (`(x) => x.length > 0`) or TS type-predicate
|
|
6
|
+
* guards (`(x): x is Foo => ...`). For type predicates, the matched handler's
|
|
7
|
+
* input is narrowed to the predicate's target type via {@link Narrow}.
|
|
8
|
+
*
|
|
9
|
+
* Note: a plain boolean guard does not advance exhaustiveness tracking — pair
|
|
10
|
+
* it with `.otherwise()`, or use a type predicate to narrow the union.
|
|
11
|
+
*
|
|
12
|
+
* @param guard - boolean or type-predicate guard function
|
|
13
|
+
* @returns the guard, usable as a {@link Pattern} in `.with`
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const sign = match<number>()
|
|
18
|
+
* .with(when((n: number) => n < 0), () => "neg")
|
|
19
|
+
* .with(when((n: number) => n >= 0), () => "non-neg")
|
|
20
|
+
* .exhaustive();
|
|
21
|
+
* sign(-1); // "neg"
|
|
22
|
+
* sign(0); // "non-neg"
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function when<T, U extends T>(guard: (input: T) => input is U): (input: T) => input is U;
|
|
26
|
+
export declare function when<T>(guard: (input: T) => boolean): Pattern<T>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onrails/pattern",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Lightweight exhaustive matching for owned tagged unions — ts-pattern-shaped DX, tree-shakeable",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alan R. Soares",
|
|
@@ -43,26 +43,36 @@
|
|
|
43
43
|
],
|
|
44
44
|
"exports": {
|
|
45
45
|
".": {
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
"import": {
|
|
47
|
+
"types": "./dist/index.d.ts",
|
|
48
|
+
"default": "./dist/index.js"
|
|
49
|
+
},
|
|
50
|
+
"require": {
|
|
51
|
+
"types": "./dist/index.d.cts",
|
|
52
|
+
"default": "./dist/index.cjs"
|
|
53
|
+
}
|
|
49
54
|
},
|
|
50
55
|
"./tag": {
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
"import": {
|
|
57
|
+
"types": "./dist/tag.d.ts",
|
|
58
|
+
"default": "./dist/tag.js"
|
|
59
|
+
},
|
|
60
|
+
"require": {
|
|
61
|
+
"types": "./dist/tag.d.cts",
|
|
62
|
+
"default": "./dist/tag.cjs"
|
|
63
|
+
}
|
|
54
64
|
}
|
|
55
65
|
},
|
|
56
66
|
"scripts": {
|
|
57
67
|
"typecheck": "tsc --noEmit",
|
|
58
68
|
"test": "bun test",
|
|
59
69
|
"check": "bun typecheck && bun test",
|
|
60
|
-
"build": "tsup",
|
|
70
|
+
"build": "tsup && tsc -p tsconfig.build.json && bun run ../../scripts/dts-to-cts.ts",
|
|
61
71
|
"clean": "rm -rf dist",
|
|
62
72
|
"prepublishOnly": "bun run build"
|
|
63
73
|
},
|
|
64
74
|
"devDependencies": {
|
|
65
|
-
"@onrails/result": "0.
|
|
75
|
+
"@onrails/result": "0.4.0",
|
|
66
76
|
"ts-expect": "^1.3.0",
|
|
67
77
|
"tsup": "^8.5.1"
|
|
68
78
|
}
|