@onrails/pattern 0.1.4 → 0.2.0
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/index.cjs +136 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +181 -6
- package/dist/index.d.ts +181 -6
- package/dist/index.js +136 -3
- package/dist/index.js.map +1 -1
- package/dist/tag.cjs.map +1 -1
- package/dist/tag.d.cts +20 -1
- package/dist/tag.d.ts +20 -1
- package/dist/tag.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -42,6 +42,11 @@ var caseForPatterns = (patterns, handler) => ({
|
|
|
42
42
|
run: handler
|
|
43
43
|
});
|
|
44
44
|
var MatchBuilder = class _MatchBuilder {
|
|
45
|
+
/**
|
|
46
|
+
* Construct a builder directly. Prefer the {@link match} factory — the
|
|
47
|
+
* constructor exists so internal combinators can thread accumulated `cases`,
|
|
48
|
+
* the optional captured `input`, and the phantom `_handled` tuple.
|
|
49
|
+
*/
|
|
45
50
|
constructor(cases, input, _handled = []) {
|
|
46
51
|
this.cases = cases;
|
|
47
52
|
this.input = input;
|
|
@@ -59,19 +64,82 @@ var MatchBuilder = class _MatchBuilder {
|
|
|
59
64
|
this._handled
|
|
60
65
|
);
|
|
61
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Add a case: when `input` satisfies `pattern`, run `handler` on the narrowed
|
|
69
|
+
* value. The pattern may be a literal, a shallow object pattern, or a guard
|
|
70
|
+
* ({@link Pattern}); a type-predicate guard narrows the handler input. Each
|
|
71
|
+
* `.with` advances compile-time exhaustiveness tracking so `.exhaustive()`
|
|
72
|
+
* knows which union members remain.
|
|
73
|
+
*
|
|
74
|
+
* @param pattern - literal, object pattern, or guard to test against
|
|
75
|
+
* @param handler - run on the narrowed input when the pattern matches
|
|
76
|
+
* @returns a builder with this case appended
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* type Event =
|
|
81
|
+
* | { type: "message"; content: string }
|
|
82
|
+
* | { type: "error"; message: string }
|
|
83
|
+
* | { type: "done" };
|
|
84
|
+
*
|
|
85
|
+
* const summary = match({ type: "message", content: "hi" } as Event)
|
|
86
|
+
* .with({ type: "message" }, (e) => e.content) // e: { type: "message"; content: string }
|
|
87
|
+
* .with({ type: "error" }, (e) => e.message)
|
|
88
|
+
* .with({ type: "done" }, () => "—")
|
|
89
|
+
* .exhaustive(); // "hi"
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
62
92
|
with(pattern, handler) {
|
|
63
93
|
return this.addCase({
|
|
64
94
|
test: (input) => matches(input, pattern),
|
|
65
95
|
run: handler
|
|
66
96
|
});
|
|
67
97
|
}
|
|
68
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* One handler for several patterns (OR). The handler input is the union of
|
|
100
|
+
* the members narrowed by each pattern, and all of them are marked handled
|
|
101
|
+
* for exhaustiveness.
|
|
102
|
+
*
|
|
103
|
+
* @param patterns - patterns to test; any match runs `handler`
|
|
104
|
+
* @param handler - run on the union of narrowed members
|
|
105
|
+
* @returns a builder with the shared case appended
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* type Job =
|
|
110
|
+
* | { kind: "queued"; id: string }
|
|
111
|
+
* | { kind: "running"; id: string }
|
|
112
|
+
* | { kind: "done"; ok: boolean };
|
|
113
|
+
*
|
|
114
|
+
* const status = match({ kind: "running", id: "j1" } as Job)
|
|
115
|
+
* .withOneOf([{ kind: "queued" }, { kind: "running" }], (j) => j.id) // j: queued | running
|
|
116
|
+
* .with({ kind: "done" }, (j) => (j.ok ? "yes" : "no"))
|
|
117
|
+
* .exhaustive(); // "j1"
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
69
120
|
withOneOf(patterns, handler) {
|
|
70
121
|
return this.addCase(
|
|
71
122
|
caseForPatterns(patterns, handler)
|
|
72
123
|
);
|
|
73
124
|
}
|
|
74
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* {@link withOneOf} for exactly two patterns — sugar that avoids the array.
|
|
127
|
+
*
|
|
128
|
+
* @param pattern1 - first pattern
|
|
129
|
+
* @param pattern2 - second pattern
|
|
130
|
+
* @param handler - run on the union of the two narrowed members
|
|
131
|
+
* @returns a builder with the shared case appended
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```ts
|
|
135
|
+
* type Provider = "ollama" | "openrouter";
|
|
136
|
+
*
|
|
137
|
+
* const pick = match<Provider>()
|
|
138
|
+
* .withEither("ollama", "openrouter", (p) => p) // p: "ollama" | "openrouter"
|
|
139
|
+
* .exhaustive();
|
|
140
|
+
* pick("ollama"); // "ollama"
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
75
143
|
withEither(pattern1, pattern2, handler) {
|
|
76
144
|
return this.withOneOf([pattern1, pattern2], handler);
|
|
77
145
|
}
|
|
@@ -97,7 +165,26 @@ var MatchBuilder = class _MatchBuilder {
|
|
|
97
165
|
returnType() {
|
|
98
166
|
return new _MatchBuilder(this.cases, this.input, this._handled);
|
|
99
167
|
}
|
|
100
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* Run the accumulated cases now, without a compile-time exhaustiveness check.
|
|
170
|
+
* Uses the value passed to {@link match} (data-first), or the value passed to
|
|
171
|
+
* `run` (curried). Throws `"Non-exhaustive match"` if no case matches at
|
|
172
|
+
* runtime — prefer {@link exhaustive} when the input is a closed union, or
|
|
173
|
+
* {@link otherwise} for an explicit fallback.
|
|
174
|
+
*
|
|
175
|
+
* @param input - the value to match when the builder was created curried
|
|
176
|
+
* @returns the matched handler's result
|
|
177
|
+
* @throws if no case matches the input
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```ts
|
|
181
|
+
* type Event = { type: "error"; message: string } | { type: "done" };
|
|
182
|
+
*
|
|
183
|
+
* const msg = match({ type: "error", message: "x" } as Event)
|
|
184
|
+
* .with({ type: "error" }, (e) => e.message)
|
|
185
|
+
* .run({ type: "error", message: "net" }); // "net"
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
101
188
|
run(...input) {
|
|
102
189
|
const value = input.length === 1 ? input[0] : this.input;
|
|
103
190
|
if (value === NO_INPUT) {
|
|
@@ -109,9 +196,55 @@ var MatchBuilder = class _MatchBuilder {
|
|
|
109
196
|
}
|
|
110
197
|
return out;
|
|
111
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Settle the match with a compile-time exhaustiveness guarantee. Only
|
|
201
|
+
* type-checks when every member of `T` has been handled; otherwise the return
|
|
202
|
+
* type becomes {@link NonExhaustiveError}, surfacing the missing cases as a
|
|
203
|
+
* type error. Returns the result directly when data-first ({@link match}
|
|
204
|
+
* called with a value), or a reusable `(input: T) => R` matcher when curried.
|
|
205
|
+
*
|
|
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
|
+
* // Curried — reusable matcher; omitting a `.with` would be a type error.
|
|
216
|
+
* const describe = match<Event>()
|
|
217
|
+
* .with({ type: "message" }, (e) => e.content)
|
|
218
|
+
* .with({ type: "error" }, (e) => `! ${e.message}`)
|
|
219
|
+
* .with({ type: "done" }, () => "done")
|
|
220
|
+
* .exhaustive();
|
|
221
|
+
* describe({ type: "done" }); // "done"
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
112
224
|
exhaustive() {
|
|
113
225
|
return this.input === NO_INPUT ? (value) => this.run(value) : this.run();
|
|
114
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Settle the match with a catch-all fallback, dropping the exhaustiveness
|
|
229
|
+
* requirement — `handler` runs (on the full input type) when no case matched.
|
|
230
|
+
* Returns the result directly when data-first, or a reusable matcher when
|
|
231
|
+
* curried.
|
|
232
|
+
*
|
|
233
|
+
* @param handler - fallback run on the input when no prior case matched
|
|
234
|
+
* @returns the result (data-first) or a matcher function (curried)
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```ts
|
|
238
|
+
* type Event =
|
|
239
|
+
* | { type: "message"; content: string }
|
|
240
|
+
* | { type: "error"; message: string }
|
|
241
|
+
* | { type: "done" };
|
|
242
|
+
*
|
|
243
|
+
* const len = match({ type: "done" } as Event)
|
|
244
|
+
* .with({ type: "message" }, (e) => e.content.length)
|
|
245
|
+
* .otherwise(() => -1); // -1
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
115
248
|
otherwise(handler) {
|
|
116
249
|
const runWithFallback = (value) => {
|
|
117
250
|
const out = runCases(value, this.cases);
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/assert.ts","../src/match.ts","../src/tag.ts","../src/when.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/** Use in `default` branches after manual narrowing — compile-time exhaustiveness check. */\nexport const assertNever = (value: never, message = \"Unreachable\"): never => {\n throw new Error(`${message}: ${String(value)}`);\n};\n","import type { ExhaustiveResult, ExhaustMatched } from \"./exhaustive.js\";\nimport type { Narrow, NarrowUnion } from \"./narrow.js\";\n\n/**\n * A pattern that matches a value of type `T`. One of:\n *\n * - **Literal** — a primitive (`\"a\"`, `42`, `true`) matched by `===`.\n * - **Object pattern** — a `Partial<T>` of shallow key/value pairs that\n * must all match by strict equality. Only available when `T` is an\n * object type.\n * - **Guard** — a function `(input: T) => boolean`. Type predicates\n * (`(x): x is U`) narrow the handler input via {@link Narrow}.\n *\n * @example\n * ```ts\n * type Event = { kind: \"click\"; x: number } | { kind: \"key\"; code: string };\n *\n * const p1: Pattern<Event> = { kind: \"click\" }; // object pattern\n * const p2: Pattern<Event> = (e) => e.kind === \"key\"; // guard\n * ```\n */\nexport type Pattern<T> = T | ObjectPattern<T> | PatternGuard<T>;\n\n// Shallow partial of `T`; restricted to object types so primitive `T`\n// (e.g. `number | string`) does not pick up `Record<string, unknown>`\n// as a candidate pattern and pollute `Narrow<T, P>` via distribution.\ntype ObjectPattern<T> = T extends object ? Partial<T> : never;\n\ntype PatternGuard<T> = (input: T) => boolean;\n\ntype Case<T, R> = {\n readonly test: (input: T) => boolean;\n readonly run: (input: T) => R;\n};\n\nconst isGuard = <T>(pattern: Pattern<T>): pattern is PatternGuard<T> =>\n typeof pattern === \"function\";\n\nconst isPatternObject = (pattern: unknown): pattern is Record<string, unknown> =>\n typeof pattern === \"object\" && pattern !== null;\n\nconst matches = <T>(input: T, pattern: Pattern<T>): boolean => {\n if (isGuard(pattern)) {\n return pattern(input);\n }\n if (!isPatternObject(pattern)) {\n return input === pattern;\n }\n if (typeof input !== \"object\" || input === null) {\n return false;\n }\n const record = input as Record<string, unknown>;\n const patternRecord = pattern as Record<string, unknown>;\n for (const key of Object.keys(patternRecord)) {\n if (record[key] !== patternRecord[key]) {\n return false;\n }\n }\n return true;\n};\n\n// Sentinel for \"no case matched\". Distinct from a handler legitimately\n// returning `undefined` (e.g. side-effect-only handlers), which would\n// otherwise be misreported as non-exhaustive.\nconst NO_MATCH = Symbol(\"@onrails/pattern/no-match\");\ntype NoMatch = typeof NO_MATCH;\n\n// Sentinel for \"curried matcher, no captured input\". Distinct from a\n// data-first `match(undefined)` / `match(null)`, which must run immediately\n// like any other value instead of silently degrading to curried mode.\nconst NO_INPUT = Symbol(\"@onrails/pattern/no-input\");\ntype NoInput = typeof NO_INPUT;\n\nconst runCases = <T, R>(input: T, cases: readonly Case<T, R>[]): R | NoMatch => {\n for (const c of cases) {\n if (c.test(input)) {\n return c.run(input);\n }\n }\n return NO_MATCH;\n};\n\nconst caseForPatterns = <T, R>(\n patterns: readonly Pattern<T>[],\n handler: (input: T) => R,\n): Case<T, R> => ({\n test: (input) => patterns.some((pattern) => matches(input, pattern)),\n run: handler,\n});\n\n// Result-type accumulator: stays at `R` when the result type is locked\n// (after `returnType<R>()`), otherwise widens with each `.with`.\ntype NextResult<Locked extends boolean, R, R2> = Locked extends true ? R : R | R2;\n\n// Handler return-type constraint: when locked, every handler must return `R`;\n// when open, the handler can return any type and the result widens to include it.\ntype HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2;\n\n/**\n * Fluent builder for {@link match}. Tracks the handled cases at the type\n * level so `.exhaustive()` only compiles when every union member is covered.\n *\n * The `Locked` phantom flag (set via `.returnType<R>()`) constrains every\n * subsequent handler to return `R`, useful when branch return-type\n * inference would widen to a union narrower than the target slot\n * (e.g. `ReactNode`, an API DTO).\n *\n * @example\n * ```ts\n * const describe = match<Event>()\n * .with({ kind: \"click\" }, (e) => `click @ ${e.x},${e.y}`)\n * .with({ kind: \"key\" }, (e) => `key ${e.code}`)\n * .exhaustive();\n * ```\n */\nexport class MatchBuilder<\n T,\n R = never,\n HasInput extends boolean = false,\n Handled extends readonly unknown[] = [],\n Locked extends boolean = false,\n> {\n constructor(\n private readonly cases: readonly Case<T, unknown>[],\n // No default: `= NO_INPUT` would swallow a data-first `undefined` input,\n // because JS applies parameter defaults to explicitly-passed undefined.\n private readonly input: T | NoInput,\n // Safe: phantom tuple — only used for compile-time exhaustiveness tracking.\n readonly _handled: Handled = [] as unknown as Handled,\n ) {}\n\n /** Appends a runtime case plus a phantom `H` entry on the handled tuple. */\n private addCase<R2, H>(\n next: Case<T, unknown>,\n ): MatchBuilder<T, NextResult<Locked, R, R2>, HasInput, readonly [...Handled, H], Locked> {\n return new MatchBuilder(\n [...this.cases, next],\n this.input,\n // Safe: _handled is a phantom tuple — only its type-level contents matter.\n this._handled as unknown as readonly [...Handled, H],\n );\n }\n\n with<const P extends Pattern<T>, R2>(\n pattern: P,\n handler: (input: Narrow<T, P>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, ExhaustMatched<T, P>],\n Locked\n > {\n return this.addCase<R2, ExhaustMatched<T, P>>({\n test: (input) => matches(input, pattern),\n run: handler as (input: T) => unknown,\n });\n }\n\n /** One handler for several patterns (OR). Handler input is the union of narrowed members. */\n withOneOf<const Ps extends readonly Pattern<T>[], R2>(\n patterns: Ps,\n handler: (input: NarrowUnion<T, Ps>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, NarrowUnion<T, Ps>],\n Locked\n > {\n return this.addCase<R2, NarrowUnion<T, Ps>>(\n caseForPatterns(patterns, handler as (input: T) => unknown),\n );\n }\n\n /** `withOneOf` for exactly two patterns. */\n withEither<const P1 extends Pattern<T>, const P2 extends Pattern<T>, R2>(\n pattern1: P1,\n pattern2: P2,\n handler: (input: NarrowUnion<T, readonly [P1, P2]>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, NarrowUnion<T, readonly [P1, P2]>],\n Locked\n > {\n return this.withOneOf([pattern1, pattern2], handler);\n }\n\n /**\n * Lock the result type. All subsequent `.with()` handlers must return `R2`.\n * Useful when branch return-type inference widens to a union narrower than\n * the slot the match feeds into (e.g. `ReactNode`).\n *\n * Also the fix for literal-widening: when a handler returns an object whose\n * field is a union literal, inference widens it (e.g. `{ mode: \"native\" }`\n * becomes `{ mode: string }`, breaking a `Mode` union). Lock the target type\n * instead of sprinkling `as const`:\n *\n * @example\n * ```ts\n * type State = { mode: \"compat\" | \"native\" };\n * const next = match(flag)\n * .returnType<State>() // handlers now checked against State\n * .with(\"--native\", () => ({ mode: \"native\" })) // no `as const` needed\n * .otherwise(() => state);\n * ```\n */\n returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true> {\n return new MatchBuilder<T, R2, HasInput, Handled, true>(this.cases, this.input, this._handled);\n }\n\n /** Run on the value passed to {@link match}, or on `input` when curried. */\n run(...input: readonly [input: T] | readonly []): R {\n const value = input.length === 1 ? input[0] : this.input;\n if (value === NO_INPUT) {\n throw new Error(\"match.run: no input value\");\n }\n const out = runCases(value, this.cases);\n if (out === NO_MATCH) {\n throw new Error(\"Non-exhaustive match: no case matched input\");\n }\n return out as R;\n }\n\n exhaustive(): ExhaustiveResult<T, Handled, R, HasInput> {\n // Safe: HasInput is true exactly when match(value) captured an input.\n return (\n this.input === NO_INPUT ? (value: T) => this.run(value) : this.run()\n ) as ExhaustiveResult<T, Handled, R, HasInput>;\n }\n\n otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R {\n const runWithFallback = (value: T): R => {\n const out = runCases(value, this.cases);\n return out === NO_MATCH ? handler(value) : (out as R);\n };\n // Safe: HasInput is true exactly when match(value) captured an input.\n return (\n this.input === NO_INPUT ? runWithFallback : runWithFallback(this.input)\n ) as HasInput extends true ? R : (input: T) => R;\n }\n}\n\n/**\n * Result-locked variant of {@link MatchBuilder}. Constructed via\n * `match(...).returnType<R>()`. Now a thin type alias over `MatchBuilder`\n * with the `Locked` phantom flag set — kept for backwards compatibility.\n */\nexport type LockedMatchBuilder<\n T,\n R,\n HasInput extends boolean = false,\n Handled extends readonly unknown[] = [],\n> = MatchBuilder<T, R, HasInput, Handled, true>;\n\n/**\n * Start a pattern-matching expression. Two call shapes:\n *\n * - `match(value)` — data-first; subsequent `.exhaustive()` or\n * `.otherwise(fn)` runs immediately against `value`.\n * - `match<T>()` — curried; subsequent `.exhaustive()` returns a function\n * `(value: T) => R` for use in `pipe(...)` or as a reusable matcher.\n *\n * @example\n * ```ts\n * // Data-first\n * const out = match({ kind: \"click\", x: 1, y: 2 } as Event)\n * .with({ kind: \"click\" }, (e) => e.x + e.y)\n * .with({ kind: \"key\" }, () => -1)\n * .exhaustive();\n *\n * // Curried — build a reusable matcher\n * const describe = match<Event>()\n * .with({ kind: \"click\" }, (e) => `click @ ${e.x},${e.y}`)\n * .with({ kind: \"key\" }, (e) => `key ${e.code}`)\n * .exhaustive();\n * ```\n */\nexport function match<T>(input: T): MatchBuilder<T, never, true>;\nexport function match<T = never>(): MatchBuilder<T, never, false>;\nexport function match<T>(\n ...input: readonly [input: T] | readonly []\n): MatchBuilder<T, never, boolean> {\n return input.length === 1\n ? new MatchBuilder<T, never, boolean>([], input[0])\n : new MatchBuilder<T, never, boolean>([], NO_INPUT);\n}\n","type Tagged = { readonly _tag: string };\n\ntype TagHandlers<T extends Tagged, R> = {\n [K in T[\"_tag\"]]: (value: Extract<T, { _tag: K }>) => R;\n};\n\n/**\n * Exhaustive match on `_tag` — for `@onrails/result` / `@onrails/maybe` style unions.\n */\nexport const matchTag = <T extends Tagged, R>(value: T, handlers: TagHandlers<T, R>): R => {\n const tag = value._tag as T[\"_tag\"];\n const handler = handlers[tag] as (value: Extract<T, { _tag: typeof tag }>) => R;\n return handler(value as Extract<T, { _tag: typeof tag }>);\n};\n","import type { Pattern } from \"./match.js\";\n\n/**\n * Guard pattern for {@link match}.with.\n *\n * Accepts plain boolean guards (`(x) => x.length > 0`) or TS type-predicate\n * guards (`(x): x is Foo => ...`). For type predicates, the matched handler's\n * input is narrowed to the predicate's target type via {@link Narrow}.\n */\nexport function when<T, U extends T>(guard: (input: T) => input is U): (input: T) => input is U;\nexport function when<T>(guard: (input: T) => boolean): Pattern<T>;\nexport function when<T>(guard: (input: T) => boolean): Pattern<T> {\n return guard;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/assert.ts","../src/match.ts","../src/tag.ts","../src/when.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Use in `default` branches (or after manual narrowing) to assert a union has\n * been fully handled. If every case is handled, `value` is `never` and the call\n * type-checks; an unhandled case makes `value` non-`never`, surfacing a compile\n * error. Always throws at runtime.\n *\n * @param value - the value that should be unreachable (typed `never`)\n * @param message - error message prefix; defaults to `\"Unreachable\"`\n * @throws always — the branch is meant to be unreachable\n *\n * @example\n * ```ts\n * type Shape = { kind: \"circle\"; r: number } | { kind: \"square\"; s: number };\n *\n * function area(shape: Shape): number {\n * switch (shape.kind) {\n * case \"circle\": return Math.PI * shape.r ** 2;\n * case \"square\": return shape.s ** 2;\n * default: return assertNever(shape); // type error if a case is added\n * }\n * }\n * ```\n */\nexport const assertNever = (value: never, message = \"Unreachable\"): never => {\n throw new Error(`${message}: ${String(value)}`);\n};\n","import type { ExhaustiveResult, ExhaustMatched } from \"./exhaustive.js\";\nimport type { Narrow, NarrowUnion } from \"./narrow.js\";\n\n/**\n * A pattern that matches a value of type `T`. One of:\n *\n * - **Literal** — a primitive (`\"a\"`, `42`, `true`) matched by `===`.\n * - **Object pattern** — a `Partial<T>` of shallow key/value pairs that\n * must all match by strict equality. Only available when `T` is an\n * object type.\n * - **Guard** — a function `(input: T) => boolean`. Type predicates\n * (`(x): x is U`) narrow the handler input via {@link Narrow}.\n *\n * @example\n * ```ts\n * type Event = { kind: \"click\"; x: number; y: number } | { kind: \"key\"; code: string };\n *\n * const p1: Pattern<Event> = { kind: \"click\" }; // object pattern\n * const p2: Pattern<Event> = (e) => e.kind === \"key\"; // guard\n * ```\n */\nexport type Pattern<T> = T | ObjectPattern<T> | PatternGuard<T>;\n\n// Shallow partial of `T`; restricted to object types so primitive `T`\n// (e.g. `number | string`) does not pick up `Record<string, unknown>`\n// as a candidate pattern and pollute `Narrow<T, P>` via distribution.\ntype ObjectPattern<T> = T extends object ? Partial<T> : never;\n\ntype PatternGuard<T> = (input: T) => boolean;\n\ntype Case<T, R> = {\n readonly test: (input: T) => boolean;\n readonly run: (input: T) => R;\n};\n\nconst isGuard = <T>(pattern: Pattern<T>): pattern is PatternGuard<T> =>\n typeof pattern === \"function\";\n\nconst isPatternObject = (pattern: unknown): pattern is Record<string, unknown> =>\n typeof pattern === \"object\" && pattern !== null;\n\nconst matches = <T>(input: T, pattern: Pattern<T>): boolean => {\n if (isGuard(pattern)) {\n return pattern(input);\n }\n if (!isPatternObject(pattern)) {\n return input === pattern;\n }\n if (typeof input !== \"object\" || input === null) {\n return false;\n }\n const record = input as Record<string, unknown>;\n const patternRecord = pattern as Record<string, unknown>;\n for (const key of Object.keys(patternRecord)) {\n if (record[key] !== patternRecord[key]) {\n return false;\n }\n }\n return true;\n};\n\n// Sentinel for \"no case matched\". Distinct from a handler legitimately\n// returning `undefined` (e.g. side-effect-only handlers), which would\n// otherwise be misreported as non-exhaustive.\nconst NO_MATCH = Symbol(\"@onrails/pattern/no-match\");\ntype NoMatch = typeof NO_MATCH;\n\n// Sentinel for \"curried matcher, no captured input\". Distinct from a\n// data-first `match(undefined)` / `match(null)`, which must run immediately\n// like any other value instead of silently degrading to curried mode.\nconst NO_INPUT = Symbol(\"@onrails/pattern/no-input\");\ntype NoInput = typeof NO_INPUT;\n\nconst runCases = <T, R>(input: T, cases: readonly Case<T, R>[]): R | NoMatch => {\n for (const c of cases) {\n if (c.test(input)) {\n return c.run(input);\n }\n }\n return NO_MATCH;\n};\n\nconst caseForPatterns = <T, R>(\n patterns: readonly Pattern<T>[],\n handler: (input: T) => R,\n): Case<T, R> => ({\n test: (input) => patterns.some((pattern) => matches(input, pattern)),\n run: handler,\n});\n\n// Result-type accumulator: stays at `R` when the result type is locked\n// (after `returnType<R>()`), otherwise widens with each `.with`.\ntype NextResult<Locked extends boolean, R, R2> = Locked extends true ? R : R | R2;\n\n// Handler return-type constraint: when locked, every handler must return `R`;\n// when open, the handler can return any type and the result widens to include it.\ntype HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2;\n\n/**\n * Fluent builder for {@link match}. Tracks the handled cases at the type\n * level so `.exhaustive()` only compiles when every union member is covered.\n *\n * The `Locked` phantom flag (set via `.returnType<R>()`) constrains every\n * subsequent handler to return `R`, useful when branch return-type\n * inference would widen to a union narrower than the target slot\n * (e.g. `ReactNode`, an API DTO).\n *\n * @example\n * ```ts\n * const describe = match<Event>()\n * .with({ kind: \"click\" }, (e) => `click @ ${e.x},${e.y}`)\n * .with({ kind: \"key\" }, (e) => `key ${e.code}`)\n * .exhaustive();\n * ```\n */\nexport class MatchBuilder<\n T,\n R = never,\n HasInput extends boolean = false,\n Handled extends readonly unknown[] = [],\n Locked extends boolean = false,\n> {\n /**\n * Construct a builder directly. Prefer the {@link match} factory — the\n * constructor exists so internal combinators can thread accumulated `cases`,\n * the optional captured `input`, and the phantom `_handled` tuple.\n */\n constructor(\n private readonly cases: readonly Case<T, unknown>[],\n // No default: `= NO_INPUT` would swallow a data-first `undefined` input,\n // because JS applies parameter defaults to explicitly-passed undefined.\n private readonly input: T | NoInput,\n /** Phantom tuple of handled cases — only its compile-time contents matter. */\n readonly _handled: Handled = [] as unknown as Handled,\n ) {}\n\n /** Appends a runtime case plus a phantom `H` entry on the handled tuple. */\n private addCase<R2, H>(\n next: Case<T, unknown>,\n ): MatchBuilder<T, NextResult<Locked, R, R2>, HasInput, readonly [...Handled, H], Locked> {\n return new MatchBuilder(\n [...this.cases, next],\n this.input,\n // Safe: _handled is a phantom tuple — only its type-level contents matter.\n this._handled as unknown as readonly [...Handled, H],\n );\n }\n\n /**\n * Add a case: when `input` satisfies `pattern`, run `handler` on the narrowed\n * value. The pattern may be a literal, a shallow object pattern, or a guard\n * ({@link Pattern}); a type-predicate guard narrows the handler input. Each\n * `.with` advances compile-time exhaustiveness tracking so `.exhaustive()`\n * knows which union members remain.\n *\n * @param pattern - literal, object pattern, or guard to test against\n * @param handler - run on the narrowed input when the pattern matches\n * @returns a builder with this case appended\n *\n * @example\n * ```ts\n * type Event =\n * | { type: \"message\"; content: string }\n * | { type: \"error\"; message: string }\n * | { type: \"done\" };\n *\n * const summary = match({ type: \"message\", content: \"hi\" } as Event)\n * .with({ type: \"message\" }, (e) => e.content) // e: { type: \"message\"; content: string }\n * .with({ type: \"error\" }, (e) => e.message)\n * .with({ type: \"done\" }, () => \"—\")\n * .exhaustive(); // \"hi\"\n * ```\n */\n with<const P extends Pattern<T>, R2>(\n pattern: P,\n handler: (input: Narrow<T, P>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, ExhaustMatched<T, P>],\n Locked\n > {\n return this.addCase<R2, ExhaustMatched<T, P>>({\n test: (input) => matches(input, pattern),\n run: handler as (input: T) => unknown,\n });\n }\n\n /**\n * One handler for several patterns (OR). The handler input is the union of\n * the members narrowed by each pattern, and all of them are marked handled\n * for exhaustiveness.\n *\n * @param patterns - patterns to test; any match runs `handler`\n * @param handler - run on the union of narrowed members\n * @returns a builder with the shared case appended\n *\n * @example\n * ```ts\n * type Job =\n * | { kind: \"queued\"; id: string }\n * | { kind: \"running\"; id: string }\n * | { kind: \"done\"; ok: boolean };\n *\n * const status = match({ kind: \"running\", id: \"j1\" } as Job)\n * .withOneOf([{ kind: \"queued\" }, { kind: \"running\" }], (j) => j.id) // j: queued | running\n * .with({ kind: \"done\" }, (j) => (j.ok ? \"yes\" : \"no\"))\n * .exhaustive(); // \"j1\"\n * ```\n */\n withOneOf<const Ps extends readonly Pattern<T>[], R2>(\n patterns: Ps,\n handler: (input: NarrowUnion<T, Ps>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, NarrowUnion<T, Ps>],\n Locked\n > {\n return this.addCase<R2, NarrowUnion<T, Ps>>(\n caseForPatterns(patterns, handler as (input: T) => unknown),\n );\n }\n\n /**\n * {@link withOneOf} for exactly two patterns — sugar that avoids the array.\n *\n * @param pattern1 - first pattern\n * @param pattern2 - second pattern\n * @param handler - run on the union of the two narrowed members\n * @returns a builder with the shared case appended\n *\n * @example\n * ```ts\n * type Provider = \"ollama\" | \"openrouter\";\n *\n * const pick = match<Provider>()\n * .withEither(\"ollama\", \"openrouter\", (p) => p) // p: \"ollama\" | \"openrouter\"\n * .exhaustive();\n * pick(\"ollama\"); // \"ollama\"\n * ```\n */\n withEither<const P1 extends Pattern<T>, const P2 extends Pattern<T>, R2>(\n pattern1: P1,\n pattern2: P2,\n handler: (input: NarrowUnion<T, readonly [P1, P2]>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, NarrowUnion<T, readonly [P1, P2]>],\n Locked\n > {\n return this.withOneOf([pattern1, pattern2], handler);\n }\n\n /**\n * Lock the result type. All subsequent `.with()` handlers must return `R2`.\n * Useful when branch return-type inference widens to a union narrower than\n * the slot the match feeds into (e.g. `ReactNode`).\n *\n * Also the fix for literal-widening: when a handler returns an object whose\n * field is a union literal, inference widens it (e.g. `{ mode: \"native\" }`\n * becomes `{ mode: string }`, breaking a `Mode` union). Lock the target type\n * instead of sprinkling `as const`:\n *\n * @example\n * ```ts\n * type State = { mode: \"compat\" | \"native\" };\n * const next = match(flag)\n * .returnType<State>() // handlers now checked against State\n * .with(\"--native\", () => ({ mode: \"native\" })) // no `as const` needed\n * .otherwise(() => state);\n * ```\n */\n returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true> {\n return new MatchBuilder<T, R2, HasInput, Handled, true>(this.cases, this.input, this._handled);\n }\n\n /**\n * Run the accumulated cases now, without a compile-time exhaustiveness check.\n * Uses the value passed to {@link match} (data-first), or the value passed to\n * `run` (curried). Throws `\"Non-exhaustive match\"` if no case matches at\n * runtime — prefer {@link exhaustive} when the input is a closed union, or\n * {@link otherwise} for an explicit fallback.\n *\n * @param input - the value to match when the builder was created curried\n * @returns the matched handler's result\n * @throws if no case matches the input\n *\n * @example\n * ```ts\n * type Event = { type: \"error\"; message: string } | { type: \"done\" };\n *\n * const msg = match({ type: \"error\", message: \"x\" } as Event)\n * .with({ type: \"error\" }, (e) => e.message)\n * .run({ type: \"error\", message: \"net\" }); // \"net\"\n * ```\n */\n run(...input: readonly [input: T] | readonly []): R {\n const value = input.length === 1 ? input[0] : this.input;\n if (value === NO_INPUT) {\n throw new Error(\"match.run: no input value\");\n }\n const out = runCases(value, this.cases);\n if (out === NO_MATCH) {\n throw new Error(\"Non-exhaustive match: no case matched input\");\n }\n return out as R;\n }\n\n /**\n * Settle the match with a compile-time exhaustiveness guarantee. Only\n * type-checks when every member of `T` has been handled; otherwise the return\n * type becomes {@link NonExhaustiveError}, surfacing the missing cases as a\n * type error. Returns the result directly when data-first ({@link match}\n * called with a value), or a reusable `(input: T) => R` matcher when curried.\n *\n * @returns the result (data-first) or a matcher function (curried)\n *\n * @example\n * ```ts\n * type Event =\n * | { type: \"message\"; content: string }\n * | { type: \"error\"; message: string }\n * | { type: \"done\" };\n *\n * // Curried — reusable matcher; omitting a `.with` would be a type error.\n * const describe = match<Event>()\n * .with({ type: \"message\" }, (e) => e.content)\n * .with({ type: \"error\" }, (e) => `! ${e.message}`)\n * .with({ type: \"done\" }, () => \"done\")\n * .exhaustive();\n * describe({ type: \"done\" }); // \"done\"\n * ```\n */\n exhaustive(): ExhaustiveResult<T, Handled, R, HasInput> {\n // Safe: HasInput is true exactly when match(value) captured an input.\n return (\n this.input === NO_INPUT ? (value: T) => this.run(value) : this.run()\n ) as ExhaustiveResult<T, Handled, R, HasInput>;\n }\n\n /**\n * Settle the match with a catch-all fallback, dropping the exhaustiveness\n * requirement — `handler` runs (on the full input type) when no case matched.\n * Returns the result directly when data-first, or a reusable matcher when\n * curried.\n *\n * @param handler - fallback run on the input when no prior case matched\n * @returns the result (data-first) or a matcher function (curried)\n *\n * @example\n * ```ts\n * type Event =\n * | { type: \"message\"; content: string }\n * | { type: \"error\"; message: string }\n * | { type: \"done\" };\n *\n * const len = match({ type: \"done\" } as Event)\n * .with({ type: \"message\" }, (e) => e.content.length)\n * .otherwise(() => -1); // -1\n * ```\n */\n otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R {\n const runWithFallback = (value: T): R => {\n const out = runCases(value, this.cases);\n return out === NO_MATCH ? handler(value) : (out as R);\n };\n // Safe: HasInput is true exactly when match(value) captured an input.\n return (\n this.input === NO_INPUT ? runWithFallback : runWithFallback(this.input)\n ) as HasInput extends true ? R : (input: T) => R;\n }\n}\n\n/**\n * Result-locked variant of {@link MatchBuilder}. Constructed via\n * `match(...).returnType<R>()`. Now a thin type alias over `MatchBuilder`\n * with the `Locked` phantom flag set — kept for backwards compatibility.\n */\nexport type LockedMatchBuilder<\n T,\n R,\n HasInput extends boolean = false,\n Handled extends readonly unknown[] = [],\n> = MatchBuilder<T, R, HasInput, Handled, true>;\n\n/**\n * Start a pattern-matching expression. Two call shapes:\n *\n * - `match(value)` — data-first; subsequent `.exhaustive()` or\n * `.otherwise(fn)` runs immediately against `value`.\n * - `match<T>()` — curried; subsequent `.exhaustive()` returns a function\n * `(value: T) => R` for use in `pipe(...)` or as a reusable matcher.\n *\n * @example\n * ```ts\n * // Data-first\n * const out = match({ kind: \"click\", x: 1, y: 2 } as Event)\n * .with({ kind: \"click\" }, (e) => e.x + e.y)\n * .with({ kind: \"key\" }, () => -1)\n * .exhaustive();\n *\n * // Curried — build a reusable matcher\n * const describe = match<Event>()\n * .with({ kind: \"click\" }, (e) => `click @ ${e.x},${e.y}`)\n * .with({ kind: \"key\" }, (e) => `key ${e.code}`)\n * .exhaustive();\n * ```\n */\nexport function match<T>(input: T): MatchBuilder<T, never, true>;\nexport function match<T = never>(): MatchBuilder<T, never, false>;\nexport function match<T>(\n ...input: readonly [input: T] | readonly []\n): MatchBuilder<T, never, boolean> {\n return input.length === 1\n ? new MatchBuilder<T, never, boolean>([], input[0])\n : new MatchBuilder<T, never, boolean>([], NO_INPUT);\n}\n","type Tagged = { readonly _tag: string };\n\ntype TagHandlers<T extends Tagged, R> = {\n [K in T[\"_tag\"]]: (value: Extract<T, { _tag: K }>) => R;\n};\n\n/**\n * Exhaustive match on `_tag` — for `@onrails/result` / `@onrails/maybe` style\n * unions. The `handlers` object must supply one branch per `_tag`, and each\n * handler receives the member narrowed to its tag.\n *\n * @param value - a tagged union value to dispatch on\n * @param handlers - one handler per `_tag`, keyed by tag\n * @returns the result of the matched handler\n *\n * @example\n * ```ts\n * import { err, ok, type Result } from \"@onrails/result\";\n *\n * const label = (r: Result<number, string>) =>\n * matchTag(r, {\n * Ok: (v) => `ok:${v.value}`, // v: Ok<number>\n * Err: (e) => `err:${e.error}`, // e: Err<string>\n * });\n * label(ok(1)); // \"ok:1\"\n * label(err(\"x\")); // \"err:x\"\n * ```\n */\nexport const matchTag = <T extends Tagged, R>(value: T, handlers: TagHandlers<T, R>): R => {\n const tag = value._tag as T[\"_tag\"];\n const handler = handlers[tag] as (value: Extract<T, { _tag: typeof tag }>) => R;\n return handler(value as Extract<T, { _tag: typeof tag }>);\n};\n","import type { Pattern } from \"./match.js\";\n\n/**\n * Guard pattern for {@link match}.with.\n *\n * Accepts plain boolean guards (`(x) => x.length > 0`) or TS type-predicate\n * guards (`(x): x is Foo => ...`). For type predicates, the matched handler's\n * input is narrowed to the predicate's target type via {@link Narrow}.\n *\n * Note: a plain boolean guard does not advance exhaustiveness tracking — pair\n * it with `.otherwise()`, or use a type predicate to narrow the union.\n *\n * @param guard - boolean or type-predicate guard function\n * @returns the guard, usable as a {@link Pattern} in `.with`\n *\n * @example\n * ```ts\n * const sign = match<number>()\n * .with(when((n) => n < 0), () => \"neg\")\n * .with(when((n) => n >= 0), () => \"non-neg\")\n * .exhaustive();\n * sign(-1); // \"neg\"\n * sign(0); // \"non-neg\"\n * ```\n */\nexport function when<T, U extends T>(guard: (input: T) => input is U): (input: T) => input is U;\nexport function when<T>(guard: (input: T) => boolean): Pattern<T>;\nexport function when<T>(guard: (input: T) => boolean): Pattern<T> {\n return guard;\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
export { matchTag } from './tag.cjs';
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Use in `default` branches (or after manual narrowing) to assert a union has
|
|
5
|
+
* been fully handled. If every case is handled, `value` is `never` and the call
|
|
6
|
+
* type-checks; an unhandled case makes `value` non-`never`, surfacing a compile
|
|
7
|
+
* error. Always throws at runtime.
|
|
8
|
+
*
|
|
9
|
+
* @param value - the value that should be unreachable (typed `never`)
|
|
10
|
+
* @param message - error message prefix; defaults to `"Unreachable"`
|
|
11
|
+
* @throws always — the branch is meant to be unreachable
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* type Shape = { kind: "circle"; r: number } | { kind: "square"; s: number };
|
|
16
|
+
*
|
|
17
|
+
* function area(shape: Shape): number {
|
|
18
|
+
* switch (shape.kind) {
|
|
19
|
+
* case "circle": return Math.PI * shape.r ** 2;
|
|
20
|
+
* case "square": return shape.s ** 2;
|
|
21
|
+
* default: return assertNever(shape); // type error if a case is added
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
4
26
|
declare const assertNever: (value: never, message?: string) => never;
|
|
5
27
|
|
|
6
28
|
type GuardTarget<F> = F extends (input: any) => input is infer U ? U : never;
|
|
@@ -19,6 +41,7 @@ type ExhaustMatched<T, P> = P extends (input: T) => boolean ? [GuardTarget<P>] e
|
|
|
19
41
|
type HandledUnion<Handled extends readonly unknown[]> = Handled[number];
|
|
20
42
|
/** Input cases not yet covered by `.with` / `.withOneOf` / `.withEither`. */
|
|
21
43
|
type RemainingCases<T, Handled extends readonly unknown[]> = Exclude<T, HandledUnion<Handled>>;
|
|
44
|
+
/** `true` when every member of `T` appears in `Handled`, otherwise `false`. */
|
|
22
45
|
type IsExhaustive<T, Handled extends readonly unknown[]> = [
|
|
23
46
|
RemainingCases<T, Handled>
|
|
24
47
|
] extends [never] ? true : false;
|
|
@@ -43,7 +66,7 @@ type ExhaustiveResult<T, Handled extends readonly unknown[], R, HasInput extends
|
|
|
43
66
|
*
|
|
44
67
|
* @example
|
|
45
68
|
* ```ts
|
|
46
|
-
* type Event = { kind: "click"; x: number } | { kind: "key"; code: string };
|
|
69
|
+
* type Event = { kind: "click"; x: number; y: number } | { kind: "key"; code: string };
|
|
47
70
|
*
|
|
48
71
|
* const p1: Pattern<Event> = { kind: "click" }; // object pattern
|
|
49
72
|
* const p2: Pattern<Event> = (e) => e.kind === "key"; // guard
|
|
@@ -80,14 +103,85 @@ type HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2
|
|
|
80
103
|
declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handled extends readonly unknown[] = [], Locked extends boolean = false> {
|
|
81
104
|
private readonly cases;
|
|
82
105
|
private readonly input;
|
|
106
|
+
/** Phantom tuple of handled cases — only its compile-time contents matter. */
|
|
83
107
|
readonly _handled: Handled;
|
|
84
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Construct a builder directly. Prefer the {@link match} factory — the
|
|
110
|
+
* constructor exists so internal combinators can thread accumulated `cases`,
|
|
111
|
+
* the optional captured `input`, and the phantom `_handled` tuple.
|
|
112
|
+
*/
|
|
113
|
+
constructor(cases: readonly Case<T, unknown>[], input: T | NoInput,
|
|
114
|
+
/** Phantom tuple of handled cases — only its compile-time contents matter. */
|
|
115
|
+
_handled?: Handled);
|
|
85
116
|
/** Appends a runtime case plus a phantom `H` entry on the handled tuple. */
|
|
86
117
|
private addCase;
|
|
118
|
+
/**
|
|
119
|
+
* Add a case: when `input` satisfies `pattern`, run `handler` on the narrowed
|
|
120
|
+
* value. The pattern may be a literal, a shallow object pattern, or a guard
|
|
121
|
+
* ({@link Pattern}); a type-predicate guard narrows the handler input. Each
|
|
122
|
+
* `.with` advances compile-time exhaustiveness tracking so `.exhaustive()`
|
|
123
|
+
* knows which union members remain.
|
|
124
|
+
*
|
|
125
|
+
* @param pattern - literal, object pattern, or guard to test against
|
|
126
|
+
* @param handler - run on the narrowed input when the pattern matches
|
|
127
|
+
* @returns a builder with this case appended
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* type Event =
|
|
132
|
+
* | { type: "message"; content: string }
|
|
133
|
+
* | { type: "error"; message: string }
|
|
134
|
+
* | { type: "done" };
|
|
135
|
+
*
|
|
136
|
+
* const summary = match({ type: "message", content: "hi" } as Event)
|
|
137
|
+
* .with({ type: "message" }, (e) => e.content) // e: { type: "message"; content: string }
|
|
138
|
+
* .with({ type: "error" }, (e) => e.message)
|
|
139
|
+
* .with({ type: "done" }, () => "—")
|
|
140
|
+
* .exhaustive(); // "hi"
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
87
143
|
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>;
|
|
88
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* One handler for several patterns (OR). The handler input is the union of
|
|
146
|
+
* the members narrowed by each pattern, and all of them are marked handled
|
|
147
|
+
* for exhaustiveness.
|
|
148
|
+
*
|
|
149
|
+
* @param patterns - patterns to test; any match runs `handler`
|
|
150
|
+
* @param handler - run on the union of narrowed members
|
|
151
|
+
* @returns a builder with the shared case appended
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```ts
|
|
155
|
+
* type Job =
|
|
156
|
+
* | { kind: "queued"; id: string }
|
|
157
|
+
* | { kind: "running"; id: string }
|
|
158
|
+
* | { kind: "done"; ok: boolean };
|
|
159
|
+
*
|
|
160
|
+
* const status = match({ kind: "running", id: "j1" } as Job)
|
|
161
|
+
* .withOneOf([{ kind: "queued" }, { kind: "running" }], (j) => j.id) // j: queued | running
|
|
162
|
+
* .with({ kind: "done" }, (j) => (j.ok ? "yes" : "no"))
|
|
163
|
+
* .exhaustive(); // "j1"
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
89
166
|
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>;
|
|
90
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* {@link withOneOf} for exactly two patterns — sugar that avoids the array.
|
|
169
|
+
*
|
|
170
|
+
* @param pattern1 - first pattern
|
|
171
|
+
* @param pattern2 - second pattern
|
|
172
|
+
* @param handler - run on the union of the two narrowed members
|
|
173
|
+
* @returns a builder with the shared case appended
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```ts
|
|
177
|
+
* type Provider = "ollama" | "openrouter";
|
|
178
|
+
*
|
|
179
|
+
* const pick = match<Provider>()
|
|
180
|
+
* .withEither("ollama", "openrouter", (p) => p) // p: "ollama" | "openrouter"
|
|
181
|
+
* .exhaustive();
|
|
182
|
+
* pick("ollama"); // "ollama"
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
91
185
|
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>;
|
|
92
186
|
/**
|
|
93
187
|
* Lock the result type. All subsequent `.with()` handlers must return `R2`.
|
|
@@ -109,9 +203,74 @@ declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handl
|
|
|
109
203
|
* ```
|
|
110
204
|
*/
|
|
111
205
|
returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true>;
|
|
112
|
-
/**
|
|
206
|
+
/**
|
|
207
|
+
* Run the accumulated cases now, without a compile-time exhaustiveness check.
|
|
208
|
+
* Uses the value passed to {@link match} (data-first), or the value passed to
|
|
209
|
+
* `run` (curried). Throws `"Non-exhaustive match"` if no case matches at
|
|
210
|
+
* runtime — prefer {@link exhaustive} when the input is a closed union, or
|
|
211
|
+
* {@link otherwise} for an explicit fallback.
|
|
212
|
+
*
|
|
213
|
+
* @param input - the value to match when the builder was created curried
|
|
214
|
+
* @returns the matched handler's result
|
|
215
|
+
* @throws if no case matches the input
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```ts
|
|
219
|
+
* type Event = { type: "error"; message: string } | { type: "done" };
|
|
220
|
+
*
|
|
221
|
+
* const msg = match({ type: "error", message: "x" } as Event)
|
|
222
|
+
* .with({ type: "error" }, (e) => e.message)
|
|
223
|
+
* .run({ type: "error", message: "net" }); // "net"
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
113
226
|
run(...input: readonly [input: T] | readonly []): R;
|
|
227
|
+
/**
|
|
228
|
+
* Settle the match with a compile-time exhaustiveness guarantee. Only
|
|
229
|
+
* type-checks when every member of `T` has been handled; otherwise the return
|
|
230
|
+
* type becomes {@link NonExhaustiveError}, surfacing the missing cases as a
|
|
231
|
+
* type error. Returns the result directly when data-first ({@link match}
|
|
232
|
+
* called with a value), or a reusable `(input: T) => R` matcher when curried.
|
|
233
|
+
*
|
|
234
|
+
* @returns the result (data-first) or a matcher function (curried)
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```ts
|
|
238
|
+
* type Event =
|
|
239
|
+
* | { type: "message"; content: string }
|
|
240
|
+
* | { type: "error"; message: string }
|
|
241
|
+
* | { type: "done" };
|
|
242
|
+
*
|
|
243
|
+
* // Curried — reusable matcher; omitting a `.with` would be a type error.
|
|
244
|
+
* const describe = match<Event>()
|
|
245
|
+
* .with({ type: "message" }, (e) => e.content)
|
|
246
|
+
* .with({ type: "error" }, (e) => `! ${e.message}`)
|
|
247
|
+
* .with({ type: "done" }, () => "done")
|
|
248
|
+
* .exhaustive();
|
|
249
|
+
* describe({ type: "done" }); // "done"
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
114
252
|
exhaustive(): ExhaustiveResult<T, Handled, R, HasInput>;
|
|
253
|
+
/**
|
|
254
|
+
* Settle the match with a catch-all fallback, dropping the exhaustiveness
|
|
255
|
+
* requirement — `handler` runs (on the full input type) when no case matched.
|
|
256
|
+
* Returns the result directly when data-first, or a reusable matcher when
|
|
257
|
+
* curried.
|
|
258
|
+
*
|
|
259
|
+
* @param handler - fallback run on the input when no prior case matched
|
|
260
|
+
* @returns the result (data-first) or a matcher function (curried)
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* ```ts
|
|
264
|
+
* type Event =
|
|
265
|
+
* | { type: "message"; content: string }
|
|
266
|
+
* | { type: "error"; message: string }
|
|
267
|
+
* | { type: "done" };
|
|
268
|
+
*
|
|
269
|
+
* const len = match({ type: "done" } as Event)
|
|
270
|
+
* .with({ type: "message" }, (e) => e.content.length)
|
|
271
|
+
* .otherwise(() => -1); // -1
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
115
274
|
otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R;
|
|
116
275
|
}
|
|
117
276
|
/**
|
|
@@ -152,6 +311,22 @@ declare function match<T = never>(): MatchBuilder<T, never, false>;
|
|
|
152
311
|
* Accepts plain boolean guards (`(x) => x.length > 0`) or TS type-predicate
|
|
153
312
|
* guards (`(x): x is Foo => ...`). For type predicates, the matched handler's
|
|
154
313
|
* input is narrowed to the predicate's target type via {@link Narrow}.
|
|
314
|
+
*
|
|
315
|
+
* Note: a plain boolean guard does not advance exhaustiveness tracking — pair
|
|
316
|
+
* it with `.otherwise()`, or use a type predicate to narrow the union.
|
|
317
|
+
*
|
|
318
|
+
* @param guard - boolean or type-predicate guard function
|
|
319
|
+
* @returns the guard, usable as a {@link Pattern} in `.with`
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```ts
|
|
323
|
+
* const sign = match<number>()
|
|
324
|
+
* .with(when((n) => n < 0), () => "neg")
|
|
325
|
+
* .with(when((n) => n >= 0), () => "non-neg")
|
|
326
|
+
* .exhaustive();
|
|
327
|
+
* sign(-1); // "neg"
|
|
328
|
+
* sign(0); // "non-neg"
|
|
329
|
+
* ```
|
|
155
330
|
*/
|
|
156
331
|
declare function when<T, U extends T>(guard: (input: T) => input is U): (input: T) => input is U;
|
|
157
332
|
declare function when<T>(guard: (input: T) => boolean): Pattern<T>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
export { matchTag } from './tag.js';
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Use in `default` branches (or after manual narrowing) to assert a union has
|
|
5
|
+
* been fully handled. If every case is handled, `value` is `never` and the call
|
|
6
|
+
* type-checks; an unhandled case makes `value` non-`never`, surfacing a compile
|
|
7
|
+
* error. Always throws at runtime.
|
|
8
|
+
*
|
|
9
|
+
* @param value - the value that should be unreachable (typed `never`)
|
|
10
|
+
* @param message - error message prefix; defaults to `"Unreachable"`
|
|
11
|
+
* @throws always — the branch is meant to be unreachable
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* type Shape = { kind: "circle"; r: number } | { kind: "square"; s: number };
|
|
16
|
+
*
|
|
17
|
+
* function area(shape: Shape): number {
|
|
18
|
+
* switch (shape.kind) {
|
|
19
|
+
* case "circle": return Math.PI * shape.r ** 2;
|
|
20
|
+
* case "square": return shape.s ** 2;
|
|
21
|
+
* default: return assertNever(shape); // type error if a case is added
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
4
26
|
declare const assertNever: (value: never, message?: string) => never;
|
|
5
27
|
|
|
6
28
|
type GuardTarget<F> = F extends (input: any) => input is infer U ? U : never;
|
|
@@ -19,6 +41,7 @@ type ExhaustMatched<T, P> = P extends (input: T) => boolean ? [GuardTarget<P>] e
|
|
|
19
41
|
type HandledUnion<Handled extends readonly unknown[]> = Handled[number];
|
|
20
42
|
/** Input cases not yet covered by `.with` / `.withOneOf` / `.withEither`. */
|
|
21
43
|
type RemainingCases<T, Handled extends readonly unknown[]> = Exclude<T, HandledUnion<Handled>>;
|
|
44
|
+
/** `true` when every member of `T` appears in `Handled`, otherwise `false`. */
|
|
22
45
|
type IsExhaustive<T, Handled extends readonly unknown[]> = [
|
|
23
46
|
RemainingCases<T, Handled>
|
|
24
47
|
] extends [never] ? true : false;
|
|
@@ -43,7 +66,7 @@ type ExhaustiveResult<T, Handled extends readonly unknown[], R, HasInput extends
|
|
|
43
66
|
*
|
|
44
67
|
* @example
|
|
45
68
|
* ```ts
|
|
46
|
-
* type Event = { kind: "click"; x: number } | { kind: "key"; code: string };
|
|
69
|
+
* type Event = { kind: "click"; x: number; y: number } | { kind: "key"; code: string };
|
|
47
70
|
*
|
|
48
71
|
* const p1: Pattern<Event> = { kind: "click" }; // object pattern
|
|
49
72
|
* const p2: Pattern<Event> = (e) => e.kind === "key"; // guard
|
|
@@ -80,14 +103,85 @@ type HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2
|
|
|
80
103
|
declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handled extends readonly unknown[] = [], Locked extends boolean = false> {
|
|
81
104
|
private readonly cases;
|
|
82
105
|
private readonly input;
|
|
106
|
+
/** Phantom tuple of handled cases — only its compile-time contents matter. */
|
|
83
107
|
readonly _handled: Handled;
|
|
84
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Construct a builder directly. Prefer the {@link match} factory — the
|
|
110
|
+
* constructor exists so internal combinators can thread accumulated `cases`,
|
|
111
|
+
* the optional captured `input`, and the phantom `_handled` tuple.
|
|
112
|
+
*/
|
|
113
|
+
constructor(cases: readonly Case<T, unknown>[], input: T | NoInput,
|
|
114
|
+
/** Phantom tuple of handled cases — only its compile-time contents matter. */
|
|
115
|
+
_handled?: Handled);
|
|
85
116
|
/** Appends a runtime case plus a phantom `H` entry on the handled tuple. */
|
|
86
117
|
private addCase;
|
|
118
|
+
/**
|
|
119
|
+
* Add a case: when `input` satisfies `pattern`, run `handler` on the narrowed
|
|
120
|
+
* value. The pattern may be a literal, a shallow object pattern, or a guard
|
|
121
|
+
* ({@link Pattern}); a type-predicate guard narrows the handler input. Each
|
|
122
|
+
* `.with` advances compile-time exhaustiveness tracking so `.exhaustive()`
|
|
123
|
+
* knows which union members remain.
|
|
124
|
+
*
|
|
125
|
+
* @param pattern - literal, object pattern, or guard to test against
|
|
126
|
+
* @param handler - run on the narrowed input when the pattern matches
|
|
127
|
+
* @returns a builder with this case appended
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* type Event =
|
|
132
|
+
* | { type: "message"; content: string }
|
|
133
|
+
* | { type: "error"; message: string }
|
|
134
|
+
* | { type: "done" };
|
|
135
|
+
*
|
|
136
|
+
* const summary = match({ type: "message", content: "hi" } as Event)
|
|
137
|
+
* .with({ type: "message" }, (e) => e.content) // e: { type: "message"; content: string }
|
|
138
|
+
* .with({ type: "error" }, (e) => e.message)
|
|
139
|
+
* .with({ type: "done" }, () => "—")
|
|
140
|
+
* .exhaustive(); // "hi"
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
87
143
|
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>;
|
|
88
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* One handler for several patterns (OR). The handler input is the union of
|
|
146
|
+
* the members narrowed by each pattern, and all of them are marked handled
|
|
147
|
+
* for exhaustiveness.
|
|
148
|
+
*
|
|
149
|
+
* @param patterns - patterns to test; any match runs `handler`
|
|
150
|
+
* @param handler - run on the union of narrowed members
|
|
151
|
+
* @returns a builder with the shared case appended
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```ts
|
|
155
|
+
* type Job =
|
|
156
|
+
* | { kind: "queued"; id: string }
|
|
157
|
+
* | { kind: "running"; id: string }
|
|
158
|
+
* | { kind: "done"; ok: boolean };
|
|
159
|
+
*
|
|
160
|
+
* const status = match({ kind: "running", id: "j1" } as Job)
|
|
161
|
+
* .withOneOf([{ kind: "queued" }, { kind: "running" }], (j) => j.id) // j: queued | running
|
|
162
|
+
* .with({ kind: "done" }, (j) => (j.ok ? "yes" : "no"))
|
|
163
|
+
* .exhaustive(); // "j1"
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
89
166
|
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>;
|
|
90
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* {@link withOneOf} for exactly two patterns — sugar that avoids the array.
|
|
169
|
+
*
|
|
170
|
+
* @param pattern1 - first pattern
|
|
171
|
+
* @param pattern2 - second pattern
|
|
172
|
+
* @param handler - run on the union of the two narrowed members
|
|
173
|
+
* @returns a builder with the shared case appended
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```ts
|
|
177
|
+
* type Provider = "ollama" | "openrouter";
|
|
178
|
+
*
|
|
179
|
+
* const pick = match<Provider>()
|
|
180
|
+
* .withEither("ollama", "openrouter", (p) => p) // p: "ollama" | "openrouter"
|
|
181
|
+
* .exhaustive();
|
|
182
|
+
* pick("ollama"); // "ollama"
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
91
185
|
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>;
|
|
92
186
|
/**
|
|
93
187
|
* Lock the result type. All subsequent `.with()` handlers must return `R2`.
|
|
@@ -109,9 +203,74 @@ declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handl
|
|
|
109
203
|
* ```
|
|
110
204
|
*/
|
|
111
205
|
returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true>;
|
|
112
|
-
/**
|
|
206
|
+
/**
|
|
207
|
+
* Run the accumulated cases now, without a compile-time exhaustiveness check.
|
|
208
|
+
* Uses the value passed to {@link match} (data-first), or the value passed to
|
|
209
|
+
* `run` (curried). Throws `"Non-exhaustive match"` if no case matches at
|
|
210
|
+
* runtime — prefer {@link exhaustive} when the input is a closed union, or
|
|
211
|
+
* {@link otherwise} for an explicit fallback.
|
|
212
|
+
*
|
|
213
|
+
* @param input - the value to match when the builder was created curried
|
|
214
|
+
* @returns the matched handler's result
|
|
215
|
+
* @throws if no case matches the input
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```ts
|
|
219
|
+
* type Event = { type: "error"; message: string } | { type: "done" };
|
|
220
|
+
*
|
|
221
|
+
* const msg = match({ type: "error", message: "x" } as Event)
|
|
222
|
+
* .with({ type: "error" }, (e) => e.message)
|
|
223
|
+
* .run({ type: "error", message: "net" }); // "net"
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
113
226
|
run(...input: readonly [input: T] | readonly []): R;
|
|
227
|
+
/**
|
|
228
|
+
* Settle the match with a compile-time exhaustiveness guarantee. Only
|
|
229
|
+
* type-checks when every member of `T` has been handled; otherwise the return
|
|
230
|
+
* type becomes {@link NonExhaustiveError}, surfacing the missing cases as a
|
|
231
|
+
* type error. Returns the result directly when data-first ({@link match}
|
|
232
|
+
* called with a value), or a reusable `(input: T) => R` matcher when curried.
|
|
233
|
+
*
|
|
234
|
+
* @returns the result (data-first) or a matcher function (curried)
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```ts
|
|
238
|
+
* type Event =
|
|
239
|
+
* | { type: "message"; content: string }
|
|
240
|
+
* | { type: "error"; message: string }
|
|
241
|
+
* | { type: "done" };
|
|
242
|
+
*
|
|
243
|
+
* // Curried — reusable matcher; omitting a `.with` would be a type error.
|
|
244
|
+
* const describe = match<Event>()
|
|
245
|
+
* .with({ type: "message" }, (e) => e.content)
|
|
246
|
+
* .with({ type: "error" }, (e) => `! ${e.message}`)
|
|
247
|
+
* .with({ type: "done" }, () => "done")
|
|
248
|
+
* .exhaustive();
|
|
249
|
+
* describe({ type: "done" }); // "done"
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
114
252
|
exhaustive(): ExhaustiveResult<T, Handled, R, HasInput>;
|
|
253
|
+
/**
|
|
254
|
+
* Settle the match with a catch-all fallback, dropping the exhaustiveness
|
|
255
|
+
* requirement — `handler` runs (on the full input type) when no case matched.
|
|
256
|
+
* Returns the result directly when data-first, or a reusable matcher when
|
|
257
|
+
* curried.
|
|
258
|
+
*
|
|
259
|
+
* @param handler - fallback run on the input when no prior case matched
|
|
260
|
+
* @returns the result (data-first) or a matcher function (curried)
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* ```ts
|
|
264
|
+
* type Event =
|
|
265
|
+
* | { type: "message"; content: string }
|
|
266
|
+
* | { type: "error"; message: string }
|
|
267
|
+
* | { type: "done" };
|
|
268
|
+
*
|
|
269
|
+
* const len = match({ type: "done" } as Event)
|
|
270
|
+
* .with({ type: "message" }, (e) => e.content.length)
|
|
271
|
+
* .otherwise(() => -1); // -1
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
115
274
|
otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R;
|
|
116
275
|
}
|
|
117
276
|
/**
|
|
@@ -152,6 +311,22 @@ declare function match<T = never>(): MatchBuilder<T, never, false>;
|
|
|
152
311
|
* Accepts plain boolean guards (`(x) => x.length > 0`) or TS type-predicate
|
|
153
312
|
* guards (`(x): x is Foo => ...`). For type predicates, the matched handler's
|
|
154
313
|
* input is narrowed to the predicate's target type via {@link Narrow}.
|
|
314
|
+
*
|
|
315
|
+
* Note: a plain boolean guard does not advance exhaustiveness tracking — pair
|
|
316
|
+
* it with `.otherwise()`, or use a type predicate to narrow the union.
|
|
317
|
+
*
|
|
318
|
+
* @param guard - boolean or type-predicate guard function
|
|
319
|
+
* @returns the guard, usable as a {@link Pattern} in `.with`
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```ts
|
|
323
|
+
* const sign = match<number>()
|
|
324
|
+
* .with(when((n) => n < 0), () => "neg")
|
|
325
|
+
* .with(when((n) => n >= 0), () => "non-neg")
|
|
326
|
+
* .exhaustive();
|
|
327
|
+
* sign(-1); // "neg"
|
|
328
|
+
* sign(0); // "non-neg"
|
|
329
|
+
* ```
|
|
155
330
|
*/
|
|
156
331
|
declare function when<T, U extends T>(guard: (input: T) => input is U): (input: T) => input is U;
|
|
157
332
|
declare function when<T>(guard: (input: T) => boolean): Pattern<T>;
|
package/dist/index.js
CHANGED
|
@@ -40,6 +40,11 @@ var caseForPatterns = (patterns, handler) => ({
|
|
|
40
40
|
run: handler
|
|
41
41
|
});
|
|
42
42
|
var MatchBuilder = class _MatchBuilder {
|
|
43
|
+
/**
|
|
44
|
+
* Construct a builder directly. Prefer the {@link match} factory — the
|
|
45
|
+
* constructor exists so internal combinators can thread accumulated `cases`,
|
|
46
|
+
* the optional captured `input`, and the phantom `_handled` tuple.
|
|
47
|
+
*/
|
|
43
48
|
constructor(cases, input, _handled = []) {
|
|
44
49
|
this.cases = cases;
|
|
45
50
|
this.input = input;
|
|
@@ -57,19 +62,82 @@ var MatchBuilder = class _MatchBuilder {
|
|
|
57
62
|
this._handled
|
|
58
63
|
);
|
|
59
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Add a case: when `input` satisfies `pattern`, run `handler` on the narrowed
|
|
67
|
+
* value. The pattern may be a literal, a shallow object pattern, or a guard
|
|
68
|
+
* ({@link Pattern}); a type-predicate guard narrows the handler input. Each
|
|
69
|
+
* `.with` advances compile-time exhaustiveness tracking so `.exhaustive()`
|
|
70
|
+
* knows which union members remain.
|
|
71
|
+
*
|
|
72
|
+
* @param pattern - literal, object pattern, or guard to test against
|
|
73
|
+
* @param handler - run on the narrowed input when the pattern matches
|
|
74
|
+
* @returns a builder with this case appended
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* type Event =
|
|
79
|
+
* | { type: "message"; content: string }
|
|
80
|
+
* | { type: "error"; message: string }
|
|
81
|
+
* | { type: "done" };
|
|
82
|
+
*
|
|
83
|
+
* const summary = match({ type: "message", content: "hi" } as Event)
|
|
84
|
+
* .with({ type: "message" }, (e) => e.content) // e: { type: "message"; content: string }
|
|
85
|
+
* .with({ type: "error" }, (e) => e.message)
|
|
86
|
+
* .with({ type: "done" }, () => "—")
|
|
87
|
+
* .exhaustive(); // "hi"
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
60
90
|
with(pattern, handler) {
|
|
61
91
|
return this.addCase({
|
|
62
92
|
test: (input) => matches(input, pattern),
|
|
63
93
|
run: handler
|
|
64
94
|
});
|
|
65
95
|
}
|
|
66
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* One handler for several patterns (OR). The handler input is the union of
|
|
98
|
+
* the members narrowed by each pattern, and all of them are marked handled
|
|
99
|
+
* for exhaustiveness.
|
|
100
|
+
*
|
|
101
|
+
* @param patterns - patterns to test; any match runs `handler`
|
|
102
|
+
* @param handler - run on the union of narrowed members
|
|
103
|
+
* @returns a builder with the shared case appended
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* type Job =
|
|
108
|
+
* | { kind: "queued"; id: string }
|
|
109
|
+
* | { kind: "running"; id: string }
|
|
110
|
+
* | { kind: "done"; ok: boolean };
|
|
111
|
+
*
|
|
112
|
+
* const status = match({ kind: "running", id: "j1" } as Job)
|
|
113
|
+
* .withOneOf([{ kind: "queued" }, { kind: "running" }], (j) => j.id) // j: queued | running
|
|
114
|
+
* .with({ kind: "done" }, (j) => (j.ok ? "yes" : "no"))
|
|
115
|
+
* .exhaustive(); // "j1"
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
67
118
|
withOneOf(patterns, handler) {
|
|
68
119
|
return this.addCase(
|
|
69
120
|
caseForPatterns(patterns, handler)
|
|
70
121
|
);
|
|
71
122
|
}
|
|
72
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* {@link withOneOf} for exactly two patterns — sugar that avoids the array.
|
|
125
|
+
*
|
|
126
|
+
* @param pattern1 - first pattern
|
|
127
|
+
* @param pattern2 - second pattern
|
|
128
|
+
* @param handler - run on the union of the two narrowed members
|
|
129
|
+
* @returns a builder with the shared case appended
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* type Provider = "ollama" | "openrouter";
|
|
134
|
+
*
|
|
135
|
+
* const pick = match<Provider>()
|
|
136
|
+
* .withEither("ollama", "openrouter", (p) => p) // p: "ollama" | "openrouter"
|
|
137
|
+
* .exhaustive();
|
|
138
|
+
* pick("ollama"); // "ollama"
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
73
141
|
withEither(pattern1, pattern2, handler) {
|
|
74
142
|
return this.withOneOf([pattern1, pattern2], handler);
|
|
75
143
|
}
|
|
@@ -95,7 +163,26 @@ var MatchBuilder = class _MatchBuilder {
|
|
|
95
163
|
returnType() {
|
|
96
164
|
return new _MatchBuilder(this.cases, this.input, this._handled);
|
|
97
165
|
}
|
|
98
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* Run the accumulated cases now, without a compile-time exhaustiveness check.
|
|
168
|
+
* Uses the value passed to {@link match} (data-first), or the value passed to
|
|
169
|
+
* `run` (curried). Throws `"Non-exhaustive match"` if no case matches at
|
|
170
|
+
* runtime — prefer {@link exhaustive} when the input is a closed union, or
|
|
171
|
+
* {@link otherwise} for an explicit fallback.
|
|
172
|
+
*
|
|
173
|
+
* @param input - the value to match when the builder was created curried
|
|
174
|
+
* @returns the matched handler's result
|
|
175
|
+
* @throws if no case matches the input
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* type Event = { type: "error"; message: string } | { type: "done" };
|
|
180
|
+
*
|
|
181
|
+
* const msg = match({ type: "error", message: "x" } as Event)
|
|
182
|
+
* .with({ type: "error" }, (e) => e.message)
|
|
183
|
+
* .run({ type: "error", message: "net" }); // "net"
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
99
186
|
run(...input) {
|
|
100
187
|
const value = input.length === 1 ? input[0] : this.input;
|
|
101
188
|
if (value === NO_INPUT) {
|
|
@@ -107,9 +194,55 @@ var MatchBuilder = class _MatchBuilder {
|
|
|
107
194
|
}
|
|
108
195
|
return out;
|
|
109
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Settle the match with a compile-time exhaustiveness guarantee. Only
|
|
199
|
+
* type-checks when every member of `T` has been handled; otherwise the return
|
|
200
|
+
* type becomes {@link NonExhaustiveError}, surfacing the missing cases as a
|
|
201
|
+
* type error. Returns the result directly when data-first ({@link match}
|
|
202
|
+
* called with a value), or a reusable `(input: T) => R` matcher when curried.
|
|
203
|
+
*
|
|
204
|
+
* @returns the result (data-first) or a matcher function (curried)
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* ```ts
|
|
208
|
+
* type Event =
|
|
209
|
+
* | { type: "message"; content: string }
|
|
210
|
+
* | { type: "error"; message: string }
|
|
211
|
+
* | { type: "done" };
|
|
212
|
+
*
|
|
213
|
+
* // Curried — reusable matcher; omitting a `.with` would be a type error.
|
|
214
|
+
* const describe = match<Event>()
|
|
215
|
+
* .with({ type: "message" }, (e) => e.content)
|
|
216
|
+
* .with({ type: "error" }, (e) => `! ${e.message}`)
|
|
217
|
+
* .with({ type: "done" }, () => "done")
|
|
218
|
+
* .exhaustive();
|
|
219
|
+
* describe({ type: "done" }); // "done"
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
110
222
|
exhaustive() {
|
|
111
223
|
return this.input === NO_INPUT ? (value) => this.run(value) : this.run();
|
|
112
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Settle the match with a catch-all fallback, dropping the exhaustiveness
|
|
227
|
+
* requirement — `handler` runs (on the full input type) when no case matched.
|
|
228
|
+
* Returns the result directly when data-first, or a reusable matcher when
|
|
229
|
+
* curried.
|
|
230
|
+
*
|
|
231
|
+
* @param handler - fallback run on the input when no prior case matched
|
|
232
|
+
* @returns the result (data-first) or a matcher function (curried)
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```ts
|
|
236
|
+
* type Event =
|
|
237
|
+
* | { type: "message"; content: string }
|
|
238
|
+
* | { type: "error"; message: string }
|
|
239
|
+
* | { type: "done" };
|
|
240
|
+
*
|
|
241
|
+
* const len = match({ type: "done" } as Event)
|
|
242
|
+
* .with({ type: "message" }, (e) => e.content.length)
|
|
243
|
+
* .otherwise(() => -1); // -1
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
113
246
|
otherwise(handler) {
|
|
114
247
|
const runWithFallback = (value) => {
|
|
115
248
|
const out = runCases(value, this.cases);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/assert.ts","../src/match.ts","../src/tag.ts","../src/when.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/** Use in `default` branches after manual narrowing — compile-time exhaustiveness check. */\nexport const assertNever = (value: never, message = \"Unreachable\"): never => {\n throw new Error(`${message}: ${String(value)}`);\n};\n","import type { ExhaustiveResult, ExhaustMatched } from \"./exhaustive.js\";\nimport type { Narrow, NarrowUnion } from \"./narrow.js\";\n\n/**\n * A pattern that matches a value of type `T`. One of:\n *\n * - **Literal** — a primitive (`\"a\"`, `42`, `true`) matched by `===`.\n * - **Object pattern** — a `Partial<T>` of shallow key/value pairs that\n * must all match by strict equality. Only available when `T` is an\n * object type.\n * - **Guard** — a function `(input: T) => boolean`. Type predicates\n * (`(x): x is U`) narrow the handler input via {@link Narrow}.\n *\n * @example\n * ```ts\n * type Event = { kind: \"click\"; x: number } | { kind: \"key\"; code: string };\n *\n * const p1: Pattern<Event> = { kind: \"click\" }; // object pattern\n * const p2: Pattern<Event> = (e) => e.kind === \"key\"; // guard\n * ```\n */\nexport type Pattern<T> = T | ObjectPattern<T> | PatternGuard<T>;\n\n// Shallow partial of `T`; restricted to object types so primitive `T`\n// (e.g. `number | string`) does not pick up `Record<string, unknown>`\n// as a candidate pattern and pollute `Narrow<T, P>` via distribution.\ntype ObjectPattern<T> = T extends object ? Partial<T> : never;\n\ntype PatternGuard<T> = (input: T) => boolean;\n\ntype Case<T, R> = {\n readonly test: (input: T) => boolean;\n readonly run: (input: T) => R;\n};\n\nconst isGuard = <T>(pattern: Pattern<T>): pattern is PatternGuard<T> =>\n typeof pattern === \"function\";\n\nconst isPatternObject = (pattern: unknown): pattern is Record<string, unknown> =>\n typeof pattern === \"object\" && pattern !== null;\n\nconst matches = <T>(input: T, pattern: Pattern<T>): boolean => {\n if (isGuard(pattern)) {\n return pattern(input);\n }\n if (!isPatternObject(pattern)) {\n return input === pattern;\n }\n if (typeof input !== \"object\" || input === null) {\n return false;\n }\n const record = input as Record<string, unknown>;\n const patternRecord = pattern as Record<string, unknown>;\n for (const key of Object.keys(patternRecord)) {\n if (record[key] !== patternRecord[key]) {\n return false;\n }\n }\n return true;\n};\n\n// Sentinel for \"no case matched\". Distinct from a handler legitimately\n// returning `undefined` (e.g. side-effect-only handlers), which would\n// otherwise be misreported as non-exhaustive.\nconst NO_MATCH = Symbol(\"@onrails/pattern/no-match\");\ntype NoMatch = typeof NO_MATCH;\n\n// Sentinel for \"curried matcher, no captured input\". Distinct from a\n// data-first `match(undefined)` / `match(null)`, which must run immediately\n// like any other value instead of silently degrading to curried mode.\nconst NO_INPUT = Symbol(\"@onrails/pattern/no-input\");\ntype NoInput = typeof NO_INPUT;\n\nconst runCases = <T, R>(input: T, cases: readonly Case<T, R>[]): R | NoMatch => {\n for (const c of cases) {\n if (c.test(input)) {\n return c.run(input);\n }\n }\n return NO_MATCH;\n};\n\nconst caseForPatterns = <T, R>(\n patterns: readonly Pattern<T>[],\n handler: (input: T) => R,\n): Case<T, R> => ({\n test: (input) => patterns.some((pattern) => matches(input, pattern)),\n run: handler,\n});\n\n// Result-type accumulator: stays at `R` when the result type is locked\n// (after `returnType<R>()`), otherwise widens with each `.with`.\ntype NextResult<Locked extends boolean, R, R2> = Locked extends true ? R : R | R2;\n\n// Handler return-type constraint: when locked, every handler must return `R`;\n// when open, the handler can return any type and the result widens to include it.\ntype HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2;\n\n/**\n * Fluent builder for {@link match}. Tracks the handled cases at the type\n * level so `.exhaustive()` only compiles when every union member is covered.\n *\n * The `Locked` phantom flag (set via `.returnType<R>()`) constrains every\n * subsequent handler to return `R`, useful when branch return-type\n * inference would widen to a union narrower than the target slot\n * (e.g. `ReactNode`, an API DTO).\n *\n * @example\n * ```ts\n * const describe = match<Event>()\n * .with({ kind: \"click\" }, (e) => `click @ ${e.x},${e.y}`)\n * .with({ kind: \"key\" }, (e) => `key ${e.code}`)\n * .exhaustive();\n * ```\n */\nexport class MatchBuilder<\n T,\n R = never,\n HasInput extends boolean = false,\n Handled extends readonly unknown[] = [],\n Locked extends boolean = false,\n> {\n constructor(\n private readonly cases: readonly Case<T, unknown>[],\n // No default: `= NO_INPUT` would swallow a data-first `undefined` input,\n // because JS applies parameter defaults to explicitly-passed undefined.\n private readonly input: T | NoInput,\n // Safe: phantom tuple — only used for compile-time exhaustiveness tracking.\n readonly _handled: Handled = [] as unknown as Handled,\n ) {}\n\n /** Appends a runtime case plus a phantom `H` entry on the handled tuple. */\n private addCase<R2, H>(\n next: Case<T, unknown>,\n ): MatchBuilder<T, NextResult<Locked, R, R2>, HasInput, readonly [...Handled, H], Locked> {\n return new MatchBuilder(\n [...this.cases, next],\n this.input,\n // Safe: _handled is a phantom tuple — only its type-level contents matter.\n this._handled as unknown as readonly [...Handled, H],\n );\n }\n\n with<const P extends Pattern<T>, R2>(\n pattern: P,\n handler: (input: Narrow<T, P>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, ExhaustMatched<T, P>],\n Locked\n > {\n return this.addCase<R2, ExhaustMatched<T, P>>({\n test: (input) => matches(input, pattern),\n run: handler as (input: T) => unknown,\n });\n }\n\n /** One handler for several patterns (OR). Handler input is the union of narrowed members. */\n withOneOf<const Ps extends readonly Pattern<T>[], R2>(\n patterns: Ps,\n handler: (input: NarrowUnion<T, Ps>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, NarrowUnion<T, Ps>],\n Locked\n > {\n return this.addCase<R2, NarrowUnion<T, Ps>>(\n caseForPatterns(patterns, handler as (input: T) => unknown),\n );\n }\n\n /** `withOneOf` for exactly two patterns. */\n withEither<const P1 extends Pattern<T>, const P2 extends Pattern<T>, R2>(\n pattern1: P1,\n pattern2: P2,\n handler: (input: NarrowUnion<T, readonly [P1, P2]>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, NarrowUnion<T, readonly [P1, P2]>],\n Locked\n > {\n return this.withOneOf([pattern1, pattern2], handler);\n }\n\n /**\n * Lock the result type. All subsequent `.with()` handlers must return `R2`.\n * Useful when branch return-type inference widens to a union narrower than\n * the slot the match feeds into (e.g. `ReactNode`).\n *\n * Also the fix for literal-widening: when a handler returns an object whose\n * field is a union literal, inference widens it (e.g. `{ mode: \"native\" }`\n * becomes `{ mode: string }`, breaking a `Mode` union). Lock the target type\n * instead of sprinkling `as const`:\n *\n * @example\n * ```ts\n * type State = { mode: \"compat\" | \"native\" };\n * const next = match(flag)\n * .returnType<State>() // handlers now checked against State\n * .with(\"--native\", () => ({ mode: \"native\" })) // no `as const` needed\n * .otherwise(() => state);\n * ```\n */\n returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true> {\n return new MatchBuilder<T, R2, HasInput, Handled, true>(this.cases, this.input, this._handled);\n }\n\n /** Run on the value passed to {@link match}, or on `input` when curried. */\n run(...input: readonly [input: T] | readonly []): R {\n const value = input.length === 1 ? input[0] : this.input;\n if (value === NO_INPUT) {\n throw new Error(\"match.run: no input value\");\n }\n const out = runCases(value, this.cases);\n if (out === NO_MATCH) {\n throw new Error(\"Non-exhaustive match: no case matched input\");\n }\n return out as R;\n }\n\n exhaustive(): ExhaustiveResult<T, Handled, R, HasInput> {\n // Safe: HasInput is true exactly when match(value) captured an input.\n return (\n this.input === NO_INPUT ? (value: T) => this.run(value) : this.run()\n ) as ExhaustiveResult<T, Handled, R, HasInput>;\n }\n\n otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R {\n const runWithFallback = (value: T): R => {\n const out = runCases(value, this.cases);\n return out === NO_MATCH ? handler(value) : (out as R);\n };\n // Safe: HasInput is true exactly when match(value) captured an input.\n return (\n this.input === NO_INPUT ? runWithFallback : runWithFallback(this.input)\n ) as HasInput extends true ? R : (input: T) => R;\n }\n}\n\n/**\n * Result-locked variant of {@link MatchBuilder}. Constructed via\n * `match(...).returnType<R>()`. Now a thin type alias over `MatchBuilder`\n * with the `Locked` phantom flag set — kept for backwards compatibility.\n */\nexport type LockedMatchBuilder<\n T,\n R,\n HasInput extends boolean = false,\n Handled extends readonly unknown[] = [],\n> = MatchBuilder<T, R, HasInput, Handled, true>;\n\n/**\n * Start a pattern-matching expression. Two call shapes:\n *\n * - `match(value)` — data-first; subsequent `.exhaustive()` or\n * `.otherwise(fn)` runs immediately against `value`.\n * - `match<T>()` — curried; subsequent `.exhaustive()` returns a function\n * `(value: T) => R` for use in `pipe(...)` or as a reusable matcher.\n *\n * @example\n * ```ts\n * // Data-first\n * const out = match({ kind: \"click\", x: 1, y: 2 } as Event)\n * .with({ kind: \"click\" }, (e) => e.x + e.y)\n * .with({ kind: \"key\" }, () => -1)\n * .exhaustive();\n *\n * // Curried — build a reusable matcher\n * const describe = match<Event>()\n * .with({ kind: \"click\" }, (e) => `click @ ${e.x},${e.y}`)\n * .with({ kind: \"key\" }, (e) => `key ${e.code}`)\n * .exhaustive();\n * ```\n */\nexport function match<T>(input: T): MatchBuilder<T, never, true>;\nexport function match<T = never>(): MatchBuilder<T, never, false>;\nexport function match<T>(\n ...input: readonly [input: T] | readonly []\n): MatchBuilder<T, never, boolean> {\n return input.length === 1\n ? new MatchBuilder<T, never, boolean>([], input[0])\n : new MatchBuilder<T, never, boolean>([], NO_INPUT);\n}\n","type Tagged = { readonly _tag: string };\n\ntype TagHandlers<T extends Tagged, R> = {\n [K in T[\"_tag\"]]: (value: Extract<T, { _tag: K }>) => R;\n};\n\n/**\n * Exhaustive match on `_tag` — for `@onrails/result` / `@onrails/maybe` style unions.\n */\nexport const matchTag = <T extends Tagged, R>(value: T, handlers: TagHandlers<T, R>): R => {\n const tag = value._tag as T[\"_tag\"];\n const handler = handlers[tag] as (value: Extract<T, { _tag: typeof tag }>) => R;\n return handler(value as Extract<T, { _tag: typeof tag }>);\n};\n","import type { Pattern } from \"./match.js\";\n\n/**\n * Guard pattern for {@link match}.with.\n *\n * Accepts plain boolean guards (`(x) => x.length > 0`) or TS type-predicate\n * guards (`(x): x is Foo => ...`). For type predicates, the matched handler's\n * input is narrowed to the predicate's target type via {@link Narrow}.\n */\nexport function when<T, U extends T>(guard: (input: T) => input is U): (input: T) => input is U;\nexport function when<T>(guard: (input: T) => boolean): Pattern<T>;\nexport function when<T>(guard: (input: T) => boolean): Pattern<T> {\n return guard;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/assert.ts","../src/match.ts","../src/tag.ts","../src/when.ts"],"names":[],"mappings":";AAuBO,IAAM,WAAA,GAAc,CAAC,KAAA,EAAc,OAAA,GAAU,aAAA,KAAyB;AAC3E,EAAA,MAAM,IAAI,MAAM,CAAA,EAAG,OAAO,KAAK,MAAA,CAAO,KAAK,CAAC,CAAA,CAAE,CAAA;AAChD;;;ACUA,IAAM,OAAA,GAAU,CAAI,OAAA,KAClB,OAAO,OAAA,KAAY,UAAA;AAErB,IAAM,kBAAkB,CAAC,OAAA,KACvB,OAAO,OAAA,KAAY,YAAY,OAAA,KAAY,IAAA;AAE7C,IAAM,OAAA,GAAU,CAAI,KAAA,EAAU,OAAA,KAAiC;AAC7D,EAAA,IAAI,OAAA,CAAQ,OAAO,CAAA,EAAG;AACpB,IAAA,OAAO,QAAQ,KAAK,CAAA;AAAA,EACtB;AACA,EAAA,IAAI,CAAC,eAAA,CAAgB,OAAO,CAAA,EAAG;AAC7B,IAAA,OAAO,KAAA,KAAU,OAAA;AAAA,EACnB;AACA,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,KAAU,IAAA,EAAM;AAC/C,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,MAAM,MAAA,GAAS,KAAA;AACf,EAAA,MAAM,aAAA,GAAgB,OAAA;AACtB,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,aAAa,CAAA,EAAG;AAC5C,IAAA,IAAI,MAAA,CAAO,GAAG,CAAA,KAAM,aAAA,CAAc,GAAG,CAAA,EAAG;AACtC,MAAA,OAAO,KAAA;AAAA,IACT;AAAA,EACF;AACA,EAAA,OAAO,IAAA;AACT,CAAA;AAKA,IAAM,QAAA,0BAAkB,2BAA2B,CAAA;AAMnD,IAAM,QAAA,0BAAkB,2BAA2B,CAAA;AAGnD,IAAM,QAAA,GAAW,CAAO,KAAA,EAAU,KAAA,KAA8C;AAC9E,EAAA,KAAA,MAAW,KAAK,KAAA,EAAO;AACrB,IAAA,IAAI,CAAA,CAAE,IAAA,CAAK,KAAK,CAAA,EAAG;AACjB,MAAA,OAAO,CAAA,CAAE,IAAI,KAAK,CAAA;AAAA,IACpB;AAAA,EACF;AACA,EAAA,OAAO,QAAA;AACT,CAAA;AAEA,IAAM,eAAA,GAAkB,CACtB,QAAA,EACA,OAAA,MACgB;AAAA,EAChB,IAAA,EAAM,CAAC,KAAA,KAAU,QAAA,CAAS,IAAA,CAAK,CAAC,OAAA,KAAY,OAAA,CAAQ,KAAA,EAAO,OAAO,CAAC,CAAA;AAAA,EACnE,GAAA,EAAK;AACP,CAAA,CAAA;AA2BO,IAAM,YAAA,GAAN,MAAM,aAAA,CAMX;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,WAAA,CACmB,KAAA,EAGA,KAAA,EAER,QAAA,GAAoB,EAAC,EAC9B;AANiB,IAAA,IAAA,CAAA,KAAA,GAAA,KAAA;AAGA,IAAA,IAAA,CAAA,KAAA,GAAA,KAAA;AAER,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AAAA,EACR;AAAA,EANgB,KAAA;AAAA,EAGA,KAAA;AAAA,EAER,QAAA;AAAA;AAAA,EAIH,QACN,IAAA,EACwF;AACxF,IAAA,OAAO,IAAI,aAAA;AAAA,MACT,CAAC,GAAG,IAAA,CAAK,KAAA,EAAO,IAAI,CAAA;AAAA,MACpB,IAAA,CAAK,KAAA;AAAA;AAAA,MAEL,IAAA,CAAK;AAAA,KACP;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA2BA,IAAA,CACE,SACA,OAAA,EAOA;AACA,IAAA,OAAO,KAAK,OAAA,CAAkC;AAAA,MAC5C,IAAA,EAAM,CAAC,KAAA,KAAU,OAAA,CAAQ,OAAO,OAAO,CAAA;AAAA,MACvC,GAAA,EAAK;AAAA,KACN,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAwBA,SAAA,CACE,UACA,OAAA,EAOA;AACA,IAAA,OAAO,IAAA,CAAK,OAAA;AAAA,MACV,eAAA,CAAgB,UAAU,OAAgC;AAAA,KAC5D;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBA,UAAA,CACE,QAAA,EACA,QAAA,EACA,OAAA,EAOA;AACA,IAAA,OAAO,KAAK,SAAA,CAAU,CAAC,QAAA,EAAU,QAAQ,GAAG,OAAO,CAAA;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqBA,UAAA,GAA+D;AAC7D,IAAA,OAAO,IAAI,aAAA,CAA6C,IAAA,CAAK,OAAO,IAAA,CAAK,KAAA,EAAO,KAAK,QAAQ,CAAA;AAAA,EAC/F;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsBA,OAAO,KAAA,EAA6C;AAClD,IAAA,MAAM,QAAQ,KAAA,CAAM,MAAA,KAAW,IAAI,KAAA,CAAM,CAAC,IAAI,IAAA,CAAK,KAAA;AACnD,IAAA,IAAI,UAAU,QAAA,EAAU;AACtB,MAAA,MAAM,IAAI,MAAM,2BAA2B,CAAA;AAAA,IAC7C;AACA,IAAA,MAAM,GAAA,GAAM,QAAA,CAAS,KAAA,EAAO,IAAA,CAAK,KAAK,CAAA;AACtC,IAAA,IAAI,QAAQ,QAAA,EAAU;AACpB,MAAA,MAAM,IAAI,MAAM,6CAA6C,CAAA;AAAA,IAC/D;AACA,IAAA,OAAO,GAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA2BA,UAAA,GAAwD;AAEtD,IAAA,OACE,IAAA,CAAK,KAAA,KAAU,QAAA,GAAW,CAAC,KAAA,KAAa,KAAK,GAAA,CAAI,KAAK,CAAA,GAAI,IAAA,CAAK,GAAA,EAAI;AAAA,EAEvE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuBA,UAAU,OAAA,EAAuE;AAC/E,IAAA,MAAM,eAAA,GAAkB,CAAC,KAAA,KAAgB;AACvC,MAAA,MAAM,GAAA,GAAM,QAAA,CAAS,KAAA,EAAO,IAAA,CAAK,KAAK,CAAA;AACtC,MAAA,OAAO,GAAA,KAAQ,QAAA,GAAW,OAAA,CAAQ,KAAK,CAAA,GAAK,GAAA;AAAA,IAC9C,CAAA;AAEA,IAAA,OACE,KAAK,KAAA,KAAU,QAAA,GAAW,eAAA,GAAkB,eAAA,CAAgB,KAAK,KAAK,CAAA;AAAA,EAE1E;AACF;AAuCO,SAAS,SACX,KAAA,EAC8B;AACjC,EAAA,OAAO,KAAA,CAAM,MAAA,KAAW,CAAA,GACpB,IAAI,aAAgC,EAAC,EAAG,KAAA,CAAM,CAAC,CAAC,CAAA,GAChD,IAAI,YAAA,CAAgC,IAAI,QAAQ,CAAA;AACtD;;;ACzYO,IAAM,QAAA,GAAW,CAAsB,KAAA,EAAU,QAAA,KAAmC;AACzF,EAAA,MAAM,MAAM,KAAA,CAAM,IAAA;AAClB,EAAA,MAAM,OAAA,GAAU,SAAS,GAAG,CAAA;AAC5B,EAAA,OAAO,QAAQ,KAAyC,CAAA;AAC1D;;;ACLO,SAAS,KAAQ,KAAA,EAA0C;AAChE,EAAA,OAAO,KAAA;AACT","file":"index.js","sourcesContent":["/**\n * Use in `default` branches (or after manual narrowing) to assert a union has\n * been fully handled. If every case is handled, `value` is `never` and the call\n * type-checks; an unhandled case makes `value` non-`never`, surfacing a compile\n * error. Always throws at runtime.\n *\n * @param value - the value that should be unreachable (typed `never`)\n * @param message - error message prefix; defaults to `\"Unreachable\"`\n * @throws always — the branch is meant to be unreachable\n *\n * @example\n * ```ts\n * type Shape = { kind: \"circle\"; r: number } | { kind: \"square\"; s: number };\n *\n * function area(shape: Shape): number {\n * switch (shape.kind) {\n * case \"circle\": return Math.PI * shape.r ** 2;\n * case \"square\": return shape.s ** 2;\n * default: return assertNever(shape); // type error if a case is added\n * }\n * }\n * ```\n */\nexport const assertNever = (value: never, message = \"Unreachable\"): never => {\n throw new Error(`${message}: ${String(value)}`);\n};\n","import type { ExhaustiveResult, ExhaustMatched } from \"./exhaustive.js\";\nimport type { Narrow, NarrowUnion } from \"./narrow.js\";\n\n/**\n * A pattern that matches a value of type `T`. One of:\n *\n * - **Literal** — a primitive (`\"a\"`, `42`, `true`) matched by `===`.\n * - **Object pattern** — a `Partial<T>` of shallow key/value pairs that\n * must all match by strict equality. Only available when `T` is an\n * object type.\n * - **Guard** — a function `(input: T) => boolean`. Type predicates\n * (`(x): x is U`) narrow the handler input via {@link Narrow}.\n *\n * @example\n * ```ts\n * type Event = { kind: \"click\"; x: number; y: number } | { kind: \"key\"; code: string };\n *\n * const p1: Pattern<Event> = { kind: \"click\" }; // object pattern\n * const p2: Pattern<Event> = (e) => e.kind === \"key\"; // guard\n * ```\n */\nexport type Pattern<T> = T | ObjectPattern<T> | PatternGuard<T>;\n\n// Shallow partial of `T`; restricted to object types so primitive `T`\n// (e.g. `number | string`) does not pick up `Record<string, unknown>`\n// as a candidate pattern and pollute `Narrow<T, P>` via distribution.\ntype ObjectPattern<T> = T extends object ? Partial<T> : never;\n\ntype PatternGuard<T> = (input: T) => boolean;\n\ntype Case<T, R> = {\n readonly test: (input: T) => boolean;\n readonly run: (input: T) => R;\n};\n\nconst isGuard = <T>(pattern: Pattern<T>): pattern is PatternGuard<T> =>\n typeof pattern === \"function\";\n\nconst isPatternObject = (pattern: unknown): pattern is Record<string, unknown> =>\n typeof pattern === \"object\" && pattern !== null;\n\nconst matches = <T>(input: T, pattern: Pattern<T>): boolean => {\n if (isGuard(pattern)) {\n return pattern(input);\n }\n if (!isPatternObject(pattern)) {\n return input === pattern;\n }\n if (typeof input !== \"object\" || input === null) {\n return false;\n }\n const record = input as Record<string, unknown>;\n const patternRecord = pattern as Record<string, unknown>;\n for (const key of Object.keys(patternRecord)) {\n if (record[key] !== patternRecord[key]) {\n return false;\n }\n }\n return true;\n};\n\n// Sentinel for \"no case matched\". Distinct from a handler legitimately\n// returning `undefined` (e.g. side-effect-only handlers), which would\n// otherwise be misreported as non-exhaustive.\nconst NO_MATCH = Symbol(\"@onrails/pattern/no-match\");\ntype NoMatch = typeof NO_MATCH;\n\n// Sentinel for \"curried matcher, no captured input\". Distinct from a\n// data-first `match(undefined)` / `match(null)`, which must run immediately\n// like any other value instead of silently degrading to curried mode.\nconst NO_INPUT = Symbol(\"@onrails/pattern/no-input\");\ntype NoInput = typeof NO_INPUT;\n\nconst runCases = <T, R>(input: T, cases: readonly Case<T, R>[]): R | NoMatch => {\n for (const c of cases) {\n if (c.test(input)) {\n return c.run(input);\n }\n }\n return NO_MATCH;\n};\n\nconst caseForPatterns = <T, R>(\n patterns: readonly Pattern<T>[],\n handler: (input: T) => R,\n): Case<T, R> => ({\n test: (input) => patterns.some((pattern) => matches(input, pattern)),\n run: handler,\n});\n\n// Result-type accumulator: stays at `R` when the result type is locked\n// (after `returnType<R>()`), otherwise widens with each `.with`.\ntype NextResult<Locked extends boolean, R, R2> = Locked extends true ? R : R | R2;\n\n// Handler return-type constraint: when locked, every handler must return `R`;\n// when open, the handler can return any type and the result widens to include it.\ntype HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2;\n\n/**\n * Fluent builder for {@link match}. Tracks the handled cases at the type\n * level so `.exhaustive()` only compiles when every union member is covered.\n *\n * The `Locked` phantom flag (set via `.returnType<R>()`) constrains every\n * subsequent handler to return `R`, useful when branch return-type\n * inference would widen to a union narrower than the target slot\n * (e.g. `ReactNode`, an API DTO).\n *\n * @example\n * ```ts\n * const describe = match<Event>()\n * .with({ kind: \"click\" }, (e) => `click @ ${e.x},${e.y}`)\n * .with({ kind: \"key\" }, (e) => `key ${e.code}`)\n * .exhaustive();\n * ```\n */\nexport class MatchBuilder<\n T,\n R = never,\n HasInput extends boolean = false,\n Handled extends readonly unknown[] = [],\n Locked extends boolean = false,\n> {\n /**\n * Construct a builder directly. Prefer the {@link match} factory — the\n * constructor exists so internal combinators can thread accumulated `cases`,\n * the optional captured `input`, and the phantom `_handled` tuple.\n */\n constructor(\n private readonly cases: readonly Case<T, unknown>[],\n // No default: `= NO_INPUT` would swallow a data-first `undefined` input,\n // because JS applies parameter defaults to explicitly-passed undefined.\n private readonly input: T | NoInput,\n /** Phantom tuple of handled cases — only its compile-time contents matter. */\n readonly _handled: Handled = [] as unknown as Handled,\n ) {}\n\n /** Appends a runtime case plus a phantom `H` entry on the handled tuple. */\n private addCase<R2, H>(\n next: Case<T, unknown>,\n ): MatchBuilder<T, NextResult<Locked, R, R2>, HasInput, readonly [...Handled, H], Locked> {\n return new MatchBuilder(\n [...this.cases, next],\n this.input,\n // Safe: _handled is a phantom tuple — only its type-level contents matter.\n this._handled as unknown as readonly [...Handled, H],\n );\n }\n\n /**\n * Add a case: when `input` satisfies `pattern`, run `handler` on the narrowed\n * value. The pattern may be a literal, a shallow object pattern, or a guard\n * ({@link Pattern}); a type-predicate guard narrows the handler input. Each\n * `.with` advances compile-time exhaustiveness tracking so `.exhaustive()`\n * knows which union members remain.\n *\n * @param pattern - literal, object pattern, or guard to test against\n * @param handler - run on the narrowed input when the pattern matches\n * @returns a builder with this case appended\n *\n * @example\n * ```ts\n * type Event =\n * | { type: \"message\"; content: string }\n * | { type: \"error\"; message: string }\n * | { type: \"done\" };\n *\n * const summary = match({ type: \"message\", content: \"hi\" } as Event)\n * .with({ type: \"message\" }, (e) => e.content) // e: { type: \"message\"; content: string }\n * .with({ type: \"error\" }, (e) => e.message)\n * .with({ type: \"done\" }, () => \"—\")\n * .exhaustive(); // \"hi\"\n * ```\n */\n with<const P extends Pattern<T>, R2>(\n pattern: P,\n handler: (input: Narrow<T, P>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, ExhaustMatched<T, P>],\n Locked\n > {\n return this.addCase<R2, ExhaustMatched<T, P>>({\n test: (input) => matches(input, pattern),\n run: handler as (input: T) => unknown,\n });\n }\n\n /**\n * One handler for several patterns (OR). The handler input is the union of\n * the members narrowed by each pattern, and all of them are marked handled\n * for exhaustiveness.\n *\n * @param patterns - patterns to test; any match runs `handler`\n * @param handler - run on the union of narrowed members\n * @returns a builder with the shared case appended\n *\n * @example\n * ```ts\n * type Job =\n * | { kind: \"queued\"; id: string }\n * | { kind: \"running\"; id: string }\n * | { kind: \"done\"; ok: boolean };\n *\n * const status = match({ kind: \"running\", id: \"j1\" } as Job)\n * .withOneOf([{ kind: \"queued\" }, { kind: \"running\" }], (j) => j.id) // j: queued | running\n * .with({ kind: \"done\" }, (j) => (j.ok ? \"yes\" : \"no\"))\n * .exhaustive(); // \"j1\"\n * ```\n */\n withOneOf<const Ps extends readonly Pattern<T>[], R2>(\n patterns: Ps,\n handler: (input: NarrowUnion<T, Ps>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, NarrowUnion<T, Ps>],\n Locked\n > {\n return this.addCase<R2, NarrowUnion<T, Ps>>(\n caseForPatterns(patterns, handler as (input: T) => unknown),\n );\n }\n\n /**\n * {@link withOneOf} for exactly two patterns — sugar that avoids the array.\n *\n * @param pattern1 - first pattern\n * @param pattern2 - second pattern\n * @param handler - run on the union of the two narrowed members\n * @returns a builder with the shared case appended\n *\n * @example\n * ```ts\n * type Provider = \"ollama\" | \"openrouter\";\n *\n * const pick = match<Provider>()\n * .withEither(\"ollama\", \"openrouter\", (p) => p) // p: \"ollama\" | \"openrouter\"\n * .exhaustive();\n * pick(\"ollama\"); // \"ollama\"\n * ```\n */\n withEither<const P1 extends Pattern<T>, const P2 extends Pattern<T>, R2>(\n pattern1: P1,\n pattern2: P2,\n handler: (input: NarrowUnion<T, readonly [P1, P2]>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, NarrowUnion<T, readonly [P1, P2]>],\n Locked\n > {\n return this.withOneOf([pattern1, pattern2], handler);\n }\n\n /**\n * Lock the result type. All subsequent `.with()` handlers must return `R2`.\n * Useful when branch return-type inference widens to a union narrower than\n * the slot the match feeds into (e.g. `ReactNode`).\n *\n * Also the fix for literal-widening: when a handler returns an object whose\n * field is a union literal, inference widens it (e.g. `{ mode: \"native\" }`\n * becomes `{ mode: string }`, breaking a `Mode` union). Lock the target type\n * instead of sprinkling `as const`:\n *\n * @example\n * ```ts\n * type State = { mode: \"compat\" | \"native\" };\n * const next = match(flag)\n * .returnType<State>() // handlers now checked against State\n * .with(\"--native\", () => ({ mode: \"native\" })) // no `as const` needed\n * .otherwise(() => state);\n * ```\n */\n returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true> {\n return new MatchBuilder<T, R2, HasInput, Handled, true>(this.cases, this.input, this._handled);\n }\n\n /**\n * Run the accumulated cases now, without a compile-time exhaustiveness check.\n * Uses the value passed to {@link match} (data-first), or the value passed to\n * `run` (curried). Throws `\"Non-exhaustive match\"` if no case matches at\n * runtime — prefer {@link exhaustive} when the input is a closed union, or\n * {@link otherwise} for an explicit fallback.\n *\n * @param input - the value to match when the builder was created curried\n * @returns the matched handler's result\n * @throws if no case matches the input\n *\n * @example\n * ```ts\n * type Event = { type: \"error\"; message: string } | { type: \"done\" };\n *\n * const msg = match({ type: \"error\", message: \"x\" } as Event)\n * .with({ type: \"error\" }, (e) => e.message)\n * .run({ type: \"error\", message: \"net\" }); // \"net\"\n * ```\n */\n run(...input: readonly [input: T] | readonly []): R {\n const value = input.length === 1 ? input[0] : this.input;\n if (value === NO_INPUT) {\n throw new Error(\"match.run: no input value\");\n }\n const out = runCases(value, this.cases);\n if (out === NO_MATCH) {\n throw new Error(\"Non-exhaustive match: no case matched input\");\n }\n return out as R;\n }\n\n /**\n * Settle the match with a compile-time exhaustiveness guarantee. Only\n * type-checks when every member of `T` has been handled; otherwise the return\n * type becomes {@link NonExhaustiveError}, surfacing the missing cases as a\n * type error. Returns the result directly when data-first ({@link match}\n * called with a value), or a reusable `(input: T) => R` matcher when curried.\n *\n * @returns the result (data-first) or a matcher function (curried)\n *\n * @example\n * ```ts\n * type Event =\n * | { type: \"message\"; content: string }\n * | { type: \"error\"; message: string }\n * | { type: \"done\" };\n *\n * // Curried — reusable matcher; omitting a `.with` would be a type error.\n * const describe = match<Event>()\n * .with({ type: \"message\" }, (e) => e.content)\n * .with({ type: \"error\" }, (e) => `! ${e.message}`)\n * .with({ type: \"done\" }, () => \"done\")\n * .exhaustive();\n * describe({ type: \"done\" }); // \"done\"\n * ```\n */\n exhaustive(): ExhaustiveResult<T, Handled, R, HasInput> {\n // Safe: HasInput is true exactly when match(value) captured an input.\n return (\n this.input === NO_INPUT ? (value: T) => this.run(value) : this.run()\n ) as ExhaustiveResult<T, Handled, R, HasInput>;\n }\n\n /**\n * Settle the match with a catch-all fallback, dropping the exhaustiveness\n * requirement — `handler` runs (on the full input type) when no case matched.\n * Returns the result directly when data-first, or a reusable matcher when\n * curried.\n *\n * @param handler - fallback run on the input when no prior case matched\n * @returns the result (data-first) or a matcher function (curried)\n *\n * @example\n * ```ts\n * type Event =\n * | { type: \"message\"; content: string }\n * | { type: \"error\"; message: string }\n * | { type: \"done\" };\n *\n * const len = match({ type: \"done\" } as Event)\n * .with({ type: \"message\" }, (e) => e.content.length)\n * .otherwise(() => -1); // -1\n * ```\n */\n otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R {\n const runWithFallback = (value: T): R => {\n const out = runCases(value, this.cases);\n return out === NO_MATCH ? handler(value) : (out as R);\n };\n // Safe: HasInput is true exactly when match(value) captured an input.\n return (\n this.input === NO_INPUT ? runWithFallback : runWithFallback(this.input)\n ) as HasInput extends true ? R : (input: T) => R;\n }\n}\n\n/**\n * Result-locked variant of {@link MatchBuilder}. Constructed via\n * `match(...).returnType<R>()`. Now a thin type alias over `MatchBuilder`\n * with the `Locked` phantom flag set — kept for backwards compatibility.\n */\nexport type LockedMatchBuilder<\n T,\n R,\n HasInput extends boolean = false,\n Handled extends readonly unknown[] = [],\n> = MatchBuilder<T, R, HasInput, Handled, true>;\n\n/**\n * Start a pattern-matching expression. Two call shapes:\n *\n * - `match(value)` — data-first; subsequent `.exhaustive()` or\n * `.otherwise(fn)` runs immediately against `value`.\n * - `match<T>()` — curried; subsequent `.exhaustive()` returns a function\n * `(value: T) => R` for use in `pipe(...)` or as a reusable matcher.\n *\n * @example\n * ```ts\n * // Data-first\n * const out = match({ kind: \"click\", x: 1, y: 2 } as Event)\n * .with({ kind: \"click\" }, (e) => e.x + e.y)\n * .with({ kind: \"key\" }, () => -1)\n * .exhaustive();\n *\n * // Curried — build a reusable matcher\n * const describe = match<Event>()\n * .with({ kind: \"click\" }, (e) => `click @ ${e.x},${e.y}`)\n * .with({ kind: \"key\" }, (e) => `key ${e.code}`)\n * .exhaustive();\n * ```\n */\nexport function match<T>(input: T): MatchBuilder<T, never, true>;\nexport function match<T = never>(): MatchBuilder<T, never, false>;\nexport function match<T>(\n ...input: readonly [input: T] | readonly []\n): MatchBuilder<T, never, boolean> {\n return input.length === 1\n ? new MatchBuilder<T, never, boolean>([], input[0])\n : new MatchBuilder<T, never, boolean>([], NO_INPUT);\n}\n","type Tagged = { readonly _tag: string };\n\ntype TagHandlers<T extends Tagged, R> = {\n [K in T[\"_tag\"]]: (value: Extract<T, { _tag: K }>) => R;\n};\n\n/**\n * Exhaustive match on `_tag` — for `@onrails/result` / `@onrails/maybe` style\n * unions. The `handlers` object must supply one branch per `_tag`, and each\n * handler receives the member narrowed to its tag.\n *\n * @param value - a tagged union value to dispatch on\n * @param handlers - one handler per `_tag`, keyed by tag\n * @returns the result of the matched handler\n *\n * @example\n * ```ts\n * import { err, ok, type Result } from \"@onrails/result\";\n *\n * const label = (r: Result<number, string>) =>\n * matchTag(r, {\n * Ok: (v) => `ok:${v.value}`, // v: Ok<number>\n * Err: (e) => `err:${e.error}`, // e: Err<string>\n * });\n * label(ok(1)); // \"ok:1\"\n * label(err(\"x\")); // \"err:x\"\n * ```\n */\nexport const matchTag = <T extends Tagged, R>(value: T, handlers: TagHandlers<T, R>): R => {\n const tag = value._tag as T[\"_tag\"];\n const handler = handlers[tag] as (value: Extract<T, { _tag: typeof tag }>) => R;\n return handler(value as Extract<T, { _tag: typeof tag }>);\n};\n","import type { Pattern } from \"./match.js\";\n\n/**\n * Guard pattern for {@link match}.with.\n *\n * Accepts plain boolean guards (`(x) => x.length > 0`) or TS type-predicate\n * guards (`(x): x is Foo => ...`). For type predicates, the matched handler's\n * input is narrowed to the predicate's target type via {@link Narrow}.\n *\n * Note: a plain boolean guard does not advance exhaustiveness tracking — pair\n * it with `.otherwise()`, or use a type predicate to narrow the union.\n *\n * @param guard - boolean or type-predicate guard function\n * @returns the guard, usable as a {@link Pattern} in `.with`\n *\n * @example\n * ```ts\n * const sign = match<number>()\n * .with(when((n) => n < 0), () => \"neg\")\n * .with(when((n) => n >= 0), () => \"non-neg\")\n * .exhaustive();\n * sign(-1); // \"neg\"\n * sign(0); // \"non-neg\"\n * ```\n */\nexport function when<T, U extends T>(guard: (input: T) => input is U): (input: T) => input is U;\nexport function when<T>(guard: (input: T) => boolean): Pattern<T>;\nexport function when<T>(guard: (input: T) => boolean): Pattern<T> {\n return guard;\n}\n"]}
|
package/dist/tag.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tag.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"sources":["../src/tag.ts"],"names":[],"mappings":";;;AA4BO,IAAM,QAAA,GAAW,CAAsB,KAAA,EAAU,QAAA,KAAmC;AACzF,EAAA,MAAM,MAAM,KAAA,CAAM,IAAA;AAClB,EAAA,MAAM,OAAA,GAAU,SAAS,GAAG,CAAA;AAC5B,EAAA,OAAO,QAAQ,KAAyC,CAAA;AAC1D","file":"tag.cjs","sourcesContent":["type Tagged = { readonly _tag: string };\n\ntype TagHandlers<T extends Tagged, R> = {\n [K in T[\"_tag\"]]: (value: Extract<T, { _tag: K }>) => R;\n};\n\n/**\n * Exhaustive match on `_tag` — for `@onrails/result` / `@onrails/maybe` style\n * unions. The `handlers` object must supply one branch per `_tag`, and each\n * handler receives the member narrowed to its tag.\n *\n * @param value - a tagged union value to dispatch on\n * @param handlers - one handler per `_tag`, keyed by tag\n * @returns the result of the matched handler\n *\n * @example\n * ```ts\n * import { err, ok, type Result } from \"@onrails/result\";\n *\n * const label = (r: Result<number, string>) =>\n * matchTag(r, {\n * Ok: (v) => `ok:${v.value}`, // v: Ok<number>\n * Err: (e) => `err:${e.error}`, // e: Err<string>\n * });\n * label(ok(1)); // \"ok:1\"\n * label(err(\"x\")); // \"err:x\"\n * ```\n */\nexport const matchTag = <T extends Tagged, R>(value: T, handlers: TagHandlers<T, R>): R => {\n const tag = value._tag as T[\"_tag\"];\n const handler = handlers[tag] as (value: Extract<T, { _tag: typeof tag }>) => R;\n return handler(value as Extract<T, { _tag: typeof tag }>);\n};\n"]}
|
package/dist/tag.d.cts
CHANGED
|
@@ -7,7 +7,26 @@ type TagHandlers<T extends Tagged, R> = {
|
|
|
7
7
|
}>) => R;
|
|
8
8
|
};
|
|
9
9
|
/**
|
|
10
|
-
* Exhaustive match on `_tag` — for `@onrails/result` / `@onrails/maybe` style
|
|
10
|
+
* Exhaustive match on `_tag` — for `@onrails/result` / `@onrails/maybe` style
|
|
11
|
+
* unions. The `handlers` object must supply one branch per `_tag`, and each
|
|
12
|
+
* handler receives the member narrowed to its tag.
|
|
13
|
+
*
|
|
14
|
+
* @param value - a tagged union value to dispatch on
|
|
15
|
+
* @param handlers - one handler per `_tag`, keyed by tag
|
|
16
|
+
* @returns the result of the matched handler
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { err, ok, type Result } from "@onrails/result";
|
|
21
|
+
*
|
|
22
|
+
* const label = (r: Result<number, string>) =>
|
|
23
|
+
* matchTag(r, {
|
|
24
|
+
* Ok: (v) => `ok:${v.value}`, // v: Ok<number>
|
|
25
|
+
* Err: (e) => `err:${e.error}`, // e: Err<string>
|
|
26
|
+
* });
|
|
27
|
+
* label(ok(1)); // "ok:1"
|
|
28
|
+
* label(err("x")); // "err:x"
|
|
29
|
+
* ```
|
|
11
30
|
*/
|
|
12
31
|
declare const matchTag: <T extends Tagged, R>(value: T, handlers: TagHandlers<T, R>) => R;
|
|
13
32
|
|
package/dist/tag.d.ts
CHANGED
|
@@ -7,7 +7,26 @@ type TagHandlers<T extends Tagged, R> = {
|
|
|
7
7
|
}>) => R;
|
|
8
8
|
};
|
|
9
9
|
/**
|
|
10
|
-
* Exhaustive match on `_tag` — for `@onrails/result` / `@onrails/maybe` style
|
|
10
|
+
* Exhaustive match on `_tag` — for `@onrails/result` / `@onrails/maybe` style
|
|
11
|
+
* unions. The `handlers` object must supply one branch per `_tag`, and each
|
|
12
|
+
* handler receives the member narrowed to its tag.
|
|
13
|
+
*
|
|
14
|
+
* @param value - a tagged union value to dispatch on
|
|
15
|
+
* @param handlers - one handler per `_tag`, keyed by tag
|
|
16
|
+
* @returns the result of the matched handler
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { err, ok, type Result } from "@onrails/result";
|
|
21
|
+
*
|
|
22
|
+
* const label = (r: Result<number, string>) =>
|
|
23
|
+
* matchTag(r, {
|
|
24
|
+
* Ok: (v) => `ok:${v.value}`, // v: Ok<number>
|
|
25
|
+
* Err: (e) => `err:${e.error}`, // e: Err<string>
|
|
26
|
+
* });
|
|
27
|
+
* label(ok(1)); // "ok:1"
|
|
28
|
+
* label(err("x")); // "err:x"
|
|
29
|
+
* ```
|
|
11
30
|
*/
|
|
12
31
|
declare const matchTag: <T extends Tagged, R>(value: T, handlers: TagHandlers<T, R>) => R;
|
|
13
32
|
|
package/dist/tag.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tag.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../src/tag.ts"],"names":[],"mappings":";AA4BO,IAAM,QAAA,GAAW,CAAsB,KAAA,EAAU,QAAA,KAAmC;AACzF,EAAA,MAAM,MAAM,KAAA,CAAM,IAAA;AAClB,EAAA,MAAM,OAAA,GAAU,SAAS,GAAG,CAAA;AAC5B,EAAA,OAAO,QAAQ,KAAyC,CAAA;AAC1D","file":"tag.js","sourcesContent":["type Tagged = { readonly _tag: string };\n\ntype TagHandlers<T extends Tagged, R> = {\n [K in T[\"_tag\"]]: (value: Extract<T, { _tag: K }>) => R;\n};\n\n/**\n * Exhaustive match on `_tag` — for `@onrails/result` / `@onrails/maybe` style\n * unions. The `handlers` object must supply one branch per `_tag`, and each\n * handler receives the member narrowed to its tag.\n *\n * @param value - a tagged union value to dispatch on\n * @param handlers - one handler per `_tag`, keyed by tag\n * @returns the result of the matched handler\n *\n * @example\n * ```ts\n * import { err, ok, type Result } from \"@onrails/result\";\n *\n * const label = (r: Result<number, string>) =>\n * matchTag(r, {\n * Ok: (v) => `ok:${v.value}`, // v: Ok<number>\n * Err: (e) => `err:${e.error}`, // e: Err<string>\n * });\n * label(ok(1)); // \"ok:1\"\n * label(err(\"x\")); // \"err:x\"\n * ```\n */\nexport const matchTag = <T extends Tagged, R>(value: T, handlers: TagHandlers<T, R>): R => {\n const tag = value._tag as T[\"_tag\"];\n const handler = handlers[tag] as (value: Extract<T, { _tag: typeof tag }>) => R;\n return handler(value as Extract<T, { _tag: typeof tag }>);\n};\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onrails/pattern",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"prepublishOnly": "bun run build"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@onrails/result": "0.1.
|
|
65
|
+
"@onrails/result": "0.1.4",
|
|
66
66
|
"ts-expect": "^1.3.0",
|
|
67
67
|
"tsup": "^8.5.1"
|
|
68
68
|
}
|