@onrails/pattern 0.1.4 → 0.3.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/DESIGN.md CHANGED
@@ -22,7 +22,7 @@ Lightweight matching for **owned** tagged unions and finite domain states. Inspi
22
22
 
23
23
  ## Result-type seeding
24
24
 
25
- `match(x).returnType<R>()` flips the builder's `Locked` phantom flag — the returned `MatchBuilder` (aliased as `LockedMatchBuilder<T, R, …>` for backwards compatibility) constrains every subsequent `.with()` handler to return `R`. Use when branch return-type inference widens to a union narrower than the slot the match feeds into (`ReactNode`, an API DTO, etc.).
25
+ `match(x).returnType<R>()` flips the builder's `Locked` phantom flag — the returned `MatchBuilder` constrains every subsequent `.with()` handler to return `R`. Use when branch return-type inference widens to a union narrower than the slot the match feeds into (`ReactNode`, an API DTO, etc.).
26
26
 
27
27
  ## Type tests
28
28
 
@@ -47,7 +47,7 @@ Runtime still throws if a value slips through (e.g. unsound cast on input).
47
47
 
48
48
  | Subpath | Purpose |
49
49
  |---------|---------|
50
- | `@onrails/pattern` | `match`, `when`, `assertNever`, `MatchBuilder`, `LockedMatchBuilder`, `Pattern`, `Narrow`, `NarrowUnion`, `RemainingCases`, `IsExhaustive`, `NonExhaustiveError` |
50
+ | `@onrails/pattern` | `match`, `when`, `assertNever`, `MatchBuilder`, `Pattern`, `Narrow`, `NarrowUnion`, `RemainingCases`, `IsExhaustive`, `NonExhaustiveError` |
51
51
  | `@onrails/pattern/tag` | `matchTag` for `_tag` dispatch |
52
52
 
53
53
  ## Migration from ts-pattern
package/README.md CHANGED
@@ -80,7 +80,7 @@ const show = <T, E>(r: Result<T, E>) =>
80
80
 
81
81
  | Path | Contents |
82
82
  |------|----------|
83
- | `@onrails/pattern` | `match`, `when`, `assertNever`, `MatchBuilder`, `LockedMatchBuilder` |
83
+ | `@onrails/pattern` | `match`, `when`, `assertNever`, `MatchBuilder` |
84
84
  | `@onrails/pattern/tag` | `matchTag` |
85
85
 
86
86
  See [DESIGN.md](./DESIGN.md).
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
- /** One handler for several patterns (OR). Handler input is the union of narrowed members. */
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
- /** `withOneOf` for exactly two patterns. */
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
- /** Run on the value passed to {@link match}, or on `input` when curried. */
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);
@@ -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 * 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: number) => n < 0), () => \"neg\")\n * .with(when((n: number) => 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
- /** Use in `default` branches after manual narrowing — compile-time exhaustiveness check. */
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,9 +41,11 @@ 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;
48
+ /** `.exhaustive()` shape: the value `R` when an input was given, else a `(input: T) => R` matcher. */
25
49
  type ExhaustiveOutput<R, HasInput extends boolean, T> = HasInput extends true ? R : (input: T) => R;
26
50
  /** Returned by `.exhaustive()` when union cases are still missing (compile-time error). */
27
51
  type NonExhaustiveError<Remaining> = {
@@ -43,7 +67,7 @@ type ExhaustiveResult<T, Handled extends readonly unknown[], R, HasInput extends
43
67
  *
44
68
  * @example
45
69
  * ```ts
46
- * type Event = { kind: "click"; x: number } | { kind: "key"; code: string };
70
+ * type Event = { kind: "click"; x: number; y: number } | { kind: "key"; code: string };
47
71
  *
48
72
  * const p1: Pattern<Event> = { kind: "click" }; // object pattern
49
73
  * const p2: Pattern<Event> = (e) => e.kind === "key"; // guard
@@ -80,14 +104,85 @@ type HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2
80
104
  declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handled extends readonly unknown[] = [], Locked extends boolean = false> {
81
105
  private readonly cases;
82
106
  private readonly input;
107
+ /** Phantom tuple of handled cases — only its compile-time contents matter. */
83
108
  readonly _handled: Handled;
84
- constructor(cases: readonly Case<T, unknown>[], input: T | NoInput, _handled?: Handled);
109
+ /**
110
+ * Construct a builder directly. Prefer the {@link match} factory — the
111
+ * constructor exists so internal combinators can thread accumulated `cases`,
112
+ * the optional captured `input`, and the phantom `_handled` tuple.
113
+ */
114
+ constructor(cases: readonly Case<T, unknown>[], input: T | NoInput,
115
+ /** Phantom tuple of handled cases — only its compile-time contents matter. */
116
+ _handled?: Handled);
85
117
  /** Appends a runtime case plus a phantom `H` entry on the handled tuple. */
86
118
  private addCase;
119
+ /**
120
+ * Add a case: when `input` satisfies `pattern`, run `handler` on the narrowed
121
+ * value. The pattern may be a literal, a shallow object pattern, or a guard
122
+ * ({@link Pattern}); a type-predicate guard narrows the handler input. Each
123
+ * `.with` advances compile-time exhaustiveness tracking so `.exhaustive()`
124
+ * knows which union members remain.
125
+ *
126
+ * @param pattern - literal, object pattern, or guard to test against
127
+ * @param handler - run on the narrowed input when the pattern matches
128
+ * @returns a builder with this case appended
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * type Event =
133
+ * | { type: "message"; content: string }
134
+ * | { type: "error"; message: string }
135
+ * | { type: "done" };
136
+ *
137
+ * const summary = match({ type: "message", content: "hi" } as Event)
138
+ * .with({ type: "message" }, (e) => e.content) // e: { type: "message"; content: string }
139
+ * .with({ type: "error" }, (e) => e.message)
140
+ * .with({ type: "done" }, () => "—")
141
+ * .exhaustive(); // "hi"
142
+ * ```
143
+ */
87
144
  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
- /** One handler for several patterns (OR). Handler input is the union of narrowed members. */
145
+ /**
146
+ * One handler for several patterns (OR). The handler input is the union of
147
+ * the members narrowed by each pattern, and all of them are marked handled
148
+ * for exhaustiveness.
149
+ *
150
+ * @param patterns - patterns to test; any match runs `handler`
151
+ * @param handler - run on the union of narrowed members
152
+ * @returns a builder with the shared case appended
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * type Job =
157
+ * | { kind: "queued"; id: string }
158
+ * | { kind: "running"; id: string }
159
+ * | { kind: "done"; ok: boolean };
160
+ *
161
+ * const status = match({ kind: "running", id: "j1" } as Job)
162
+ * .withOneOf([{ kind: "queued" }, { kind: "running" }], (j) => j.id) // j: queued | running
163
+ * .with({ kind: "done" }, (j) => (j.ok ? "yes" : "no"))
164
+ * .exhaustive(); // "j1"
165
+ * ```
166
+ */
89
167
  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
- /** `withOneOf` for exactly two patterns. */
168
+ /**
169
+ * {@link withOneOf} for exactly two patterns — sugar that avoids the array.
170
+ *
171
+ * @param pattern1 - first pattern
172
+ * @param pattern2 - second pattern
173
+ * @param handler - run on the union of the two narrowed members
174
+ * @returns a builder with the shared case appended
175
+ *
176
+ * @example
177
+ * ```ts
178
+ * type Provider = "ollama" | "openrouter";
179
+ *
180
+ * const pick = match<Provider>()
181
+ * .withEither("ollama", "openrouter", (p) => p) // p: "ollama" | "openrouter"
182
+ * .exhaustive();
183
+ * pick("ollama"); // "ollama"
184
+ * ```
185
+ */
91
186
  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
187
  /**
93
188
  * Lock the result type. All subsequent `.with()` handlers must return `R2`.
@@ -109,17 +204,76 @@ declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handl
109
204
  * ```
110
205
  */
111
206
  returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true>;
112
- /** Run on the value passed to {@link match}, or on `input` when curried. */
207
+ /**
208
+ * Run the accumulated cases now, without a compile-time exhaustiveness check.
209
+ * Uses the value passed to {@link match} (data-first), or the value passed to
210
+ * `run` (curried). Throws `"Non-exhaustive match"` if no case matches at
211
+ * runtime — prefer {@link exhaustive} when the input is a closed union, or
212
+ * {@link otherwise} for an explicit fallback.
213
+ *
214
+ * @param input - the value to match when the builder was created curried
215
+ * @returns the matched handler's result
216
+ * @throws if no case matches the input
217
+ *
218
+ * @example
219
+ * ```ts
220
+ * type Event = { type: "error"; message: string } | { type: "done" };
221
+ *
222
+ * const msg = match({ type: "error", message: "x" } as Event)
223
+ * .with({ type: "error" }, (e) => e.message)
224
+ * .run({ type: "error", message: "net" }); // "net"
225
+ * ```
226
+ */
113
227
  run(...input: readonly [input: T] | readonly []): R;
228
+ /**
229
+ * Settle the match with a compile-time exhaustiveness guarantee. Only
230
+ * type-checks when every member of `T` has been handled; otherwise the return
231
+ * type becomes {@link NonExhaustiveError}, surfacing the missing cases as a
232
+ * type error. Returns the result directly when data-first ({@link match}
233
+ * called with a value), or a reusable `(input: T) => R` matcher when curried.
234
+ *
235
+ * @returns the result (data-first) or a matcher function (curried)
236
+ *
237
+ * @example
238
+ * ```ts
239
+ * type Event =
240
+ * | { type: "message"; content: string }
241
+ * | { type: "error"; message: string }
242
+ * | { type: "done" };
243
+ *
244
+ * // Curried — reusable matcher; omitting a `.with` would be a type error.
245
+ * const describe = match<Event>()
246
+ * .with({ type: "message" }, (e) => e.content)
247
+ * .with({ type: "error" }, (e) => `! ${e.message}`)
248
+ * .with({ type: "done" }, () => "done")
249
+ * .exhaustive();
250
+ * describe({ type: "done" }); // "done"
251
+ * ```
252
+ */
114
253
  exhaustive(): ExhaustiveResult<T, Handled, R, HasInput>;
254
+ /**
255
+ * Settle the match with a catch-all fallback, dropping the exhaustiveness
256
+ * requirement — `handler` runs (on the full input type) when no case matched.
257
+ * Returns the result directly when data-first, or a reusable matcher when
258
+ * curried.
259
+ *
260
+ * @param handler - fallback run on the input when no prior case matched
261
+ * @returns the result (data-first) or a matcher function (curried)
262
+ *
263
+ * @example
264
+ * ```ts
265
+ * type Event =
266
+ * | { type: "message"; content: string }
267
+ * | { type: "error"; message: string }
268
+ * | { type: "done" };
269
+ *
270
+ * const len = match({ type: "done" } as Event)
271
+ * .with({ type: "message" }, (e) => e.content.length)
272
+ * .otherwise(() => -1); // -1
273
+ * ```
274
+ */
115
275
  otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R;
116
276
  }
117
- /**
118
- * Result-locked variant of {@link MatchBuilder}. Constructed via
119
- * `match(...).returnType<R>()`. Now a thin type alias over `MatchBuilder`
120
- * with the `Locked` phantom flag set — kept for backwards compatibility.
121
- */
122
- type LockedMatchBuilder<T, R, HasInput extends boolean = false, Handled extends readonly unknown[] = []> = MatchBuilder<T, R, HasInput, Handled, true>;
123
277
  /**
124
278
  * Start a pattern-matching expression. Two call shapes:
125
279
  *
@@ -152,8 +306,24 @@ declare function match<T = never>(): MatchBuilder<T, never, false>;
152
306
  * Accepts plain boolean guards (`(x) => x.length > 0`) or TS type-predicate
153
307
  * guards (`(x): x is Foo => ...`). For type predicates, the matched handler's
154
308
  * input is narrowed to the predicate's target type via {@link Narrow}.
309
+ *
310
+ * Note: a plain boolean guard does not advance exhaustiveness tracking — pair
311
+ * it with `.otherwise()`, or use a type predicate to narrow the union.
312
+ *
313
+ * @param guard - boolean or type-predicate guard function
314
+ * @returns the guard, usable as a {@link Pattern} in `.with`
315
+ *
316
+ * @example
317
+ * ```ts
318
+ * const sign = match<number>()
319
+ * .with(when((n: number) => n < 0), () => "neg")
320
+ * .with(when((n: number) => n >= 0), () => "non-neg")
321
+ * .exhaustive();
322
+ * sign(-1); // "neg"
323
+ * sign(0); // "non-neg"
324
+ * ```
155
325
  */
156
326
  declare function when<T, U extends T>(guard: (input: T) => input is U): (input: T) => input is U;
157
327
  declare function when<T>(guard: (input: T) => boolean): Pattern<T>;
158
328
 
159
- export { type ExhaustMatched, type ExhaustiveResult, type HandledUnion, type IsExhaustive, type LockedMatchBuilder, MatchBuilder, type Narrow, type NarrowUnion, type NonExhaustiveError, type Pattern, type RemainingCases, assertNever, match, when };
329
+ export { type ExhaustMatched, type ExhaustiveResult, type HandledUnion, type IsExhaustive, MatchBuilder, type Narrow, type NarrowUnion, type NonExhaustiveError, type Pattern, type RemainingCases, assertNever, match, when };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,28 @@
1
1
  export { matchTag } from './tag.js';
2
2
 
3
- /** Use in `default` branches after manual narrowing — compile-time exhaustiveness check. */
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,9 +41,11 @@ 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;
48
+ /** `.exhaustive()` shape: the value `R` when an input was given, else a `(input: T) => R` matcher. */
25
49
  type ExhaustiveOutput<R, HasInput extends boolean, T> = HasInput extends true ? R : (input: T) => R;
26
50
  /** Returned by `.exhaustive()` when union cases are still missing (compile-time error). */
27
51
  type NonExhaustiveError<Remaining> = {
@@ -43,7 +67,7 @@ type ExhaustiveResult<T, Handled extends readonly unknown[], R, HasInput extends
43
67
  *
44
68
  * @example
45
69
  * ```ts
46
- * type Event = { kind: "click"; x: number } | { kind: "key"; code: string };
70
+ * type Event = { kind: "click"; x: number; y: number } | { kind: "key"; code: string };
47
71
  *
48
72
  * const p1: Pattern<Event> = { kind: "click" }; // object pattern
49
73
  * const p2: Pattern<Event> = (e) => e.kind === "key"; // guard
@@ -80,14 +104,85 @@ type HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2
80
104
  declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handled extends readonly unknown[] = [], Locked extends boolean = false> {
81
105
  private readonly cases;
82
106
  private readonly input;
107
+ /** Phantom tuple of handled cases — only its compile-time contents matter. */
83
108
  readonly _handled: Handled;
84
- constructor(cases: readonly Case<T, unknown>[], input: T | NoInput, _handled?: Handled);
109
+ /**
110
+ * Construct a builder directly. Prefer the {@link match} factory — the
111
+ * constructor exists so internal combinators can thread accumulated `cases`,
112
+ * the optional captured `input`, and the phantom `_handled` tuple.
113
+ */
114
+ constructor(cases: readonly Case<T, unknown>[], input: T | NoInput,
115
+ /** Phantom tuple of handled cases — only its compile-time contents matter. */
116
+ _handled?: Handled);
85
117
  /** Appends a runtime case plus a phantom `H` entry on the handled tuple. */
86
118
  private addCase;
119
+ /**
120
+ * Add a case: when `input` satisfies `pattern`, run `handler` on the narrowed
121
+ * value. The pattern may be a literal, a shallow object pattern, or a guard
122
+ * ({@link Pattern}); a type-predicate guard narrows the handler input. Each
123
+ * `.with` advances compile-time exhaustiveness tracking so `.exhaustive()`
124
+ * knows which union members remain.
125
+ *
126
+ * @param pattern - literal, object pattern, or guard to test against
127
+ * @param handler - run on the narrowed input when the pattern matches
128
+ * @returns a builder with this case appended
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * type Event =
133
+ * | { type: "message"; content: string }
134
+ * | { type: "error"; message: string }
135
+ * | { type: "done" };
136
+ *
137
+ * const summary = match({ type: "message", content: "hi" } as Event)
138
+ * .with({ type: "message" }, (e) => e.content) // e: { type: "message"; content: string }
139
+ * .with({ type: "error" }, (e) => e.message)
140
+ * .with({ type: "done" }, () => "—")
141
+ * .exhaustive(); // "hi"
142
+ * ```
143
+ */
87
144
  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
- /** One handler for several patterns (OR). Handler input is the union of narrowed members. */
145
+ /**
146
+ * One handler for several patterns (OR). The handler input is the union of
147
+ * the members narrowed by each pattern, and all of them are marked handled
148
+ * for exhaustiveness.
149
+ *
150
+ * @param patterns - patterns to test; any match runs `handler`
151
+ * @param handler - run on the union of narrowed members
152
+ * @returns a builder with the shared case appended
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * type Job =
157
+ * | { kind: "queued"; id: string }
158
+ * | { kind: "running"; id: string }
159
+ * | { kind: "done"; ok: boolean };
160
+ *
161
+ * const status = match({ kind: "running", id: "j1" } as Job)
162
+ * .withOneOf([{ kind: "queued" }, { kind: "running" }], (j) => j.id) // j: queued | running
163
+ * .with({ kind: "done" }, (j) => (j.ok ? "yes" : "no"))
164
+ * .exhaustive(); // "j1"
165
+ * ```
166
+ */
89
167
  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
- /** `withOneOf` for exactly two patterns. */
168
+ /**
169
+ * {@link withOneOf} for exactly two patterns — sugar that avoids the array.
170
+ *
171
+ * @param pattern1 - first pattern
172
+ * @param pattern2 - second pattern
173
+ * @param handler - run on the union of the two narrowed members
174
+ * @returns a builder with the shared case appended
175
+ *
176
+ * @example
177
+ * ```ts
178
+ * type Provider = "ollama" | "openrouter";
179
+ *
180
+ * const pick = match<Provider>()
181
+ * .withEither("ollama", "openrouter", (p) => p) // p: "ollama" | "openrouter"
182
+ * .exhaustive();
183
+ * pick("ollama"); // "ollama"
184
+ * ```
185
+ */
91
186
  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
187
  /**
93
188
  * Lock the result type. All subsequent `.with()` handlers must return `R2`.
@@ -109,17 +204,76 @@ declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handl
109
204
  * ```
110
205
  */
111
206
  returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true>;
112
- /** Run on the value passed to {@link match}, or on `input` when curried. */
207
+ /**
208
+ * Run the accumulated cases now, without a compile-time exhaustiveness check.
209
+ * Uses the value passed to {@link match} (data-first), or the value passed to
210
+ * `run` (curried). Throws `"Non-exhaustive match"` if no case matches at
211
+ * runtime — prefer {@link exhaustive} when the input is a closed union, or
212
+ * {@link otherwise} for an explicit fallback.
213
+ *
214
+ * @param input - the value to match when the builder was created curried
215
+ * @returns the matched handler's result
216
+ * @throws if no case matches the input
217
+ *
218
+ * @example
219
+ * ```ts
220
+ * type Event = { type: "error"; message: string } | { type: "done" };
221
+ *
222
+ * const msg = match({ type: "error", message: "x" } as Event)
223
+ * .with({ type: "error" }, (e) => e.message)
224
+ * .run({ type: "error", message: "net" }); // "net"
225
+ * ```
226
+ */
113
227
  run(...input: readonly [input: T] | readonly []): R;
228
+ /**
229
+ * Settle the match with a compile-time exhaustiveness guarantee. Only
230
+ * type-checks when every member of `T` has been handled; otherwise the return
231
+ * type becomes {@link NonExhaustiveError}, surfacing the missing cases as a
232
+ * type error. Returns the result directly when data-first ({@link match}
233
+ * called with a value), or a reusable `(input: T) => R` matcher when curried.
234
+ *
235
+ * @returns the result (data-first) or a matcher function (curried)
236
+ *
237
+ * @example
238
+ * ```ts
239
+ * type Event =
240
+ * | { type: "message"; content: string }
241
+ * | { type: "error"; message: string }
242
+ * | { type: "done" };
243
+ *
244
+ * // Curried — reusable matcher; omitting a `.with` would be a type error.
245
+ * const describe = match<Event>()
246
+ * .with({ type: "message" }, (e) => e.content)
247
+ * .with({ type: "error" }, (e) => `! ${e.message}`)
248
+ * .with({ type: "done" }, () => "done")
249
+ * .exhaustive();
250
+ * describe({ type: "done" }); // "done"
251
+ * ```
252
+ */
114
253
  exhaustive(): ExhaustiveResult<T, Handled, R, HasInput>;
254
+ /**
255
+ * Settle the match with a catch-all fallback, dropping the exhaustiveness
256
+ * requirement — `handler` runs (on the full input type) when no case matched.
257
+ * Returns the result directly when data-first, or a reusable matcher when
258
+ * curried.
259
+ *
260
+ * @param handler - fallback run on the input when no prior case matched
261
+ * @returns the result (data-first) or a matcher function (curried)
262
+ *
263
+ * @example
264
+ * ```ts
265
+ * type Event =
266
+ * | { type: "message"; content: string }
267
+ * | { type: "error"; message: string }
268
+ * | { type: "done" };
269
+ *
270
+ * const len = match({ type: "done" } as Event)
271
+ * .with({ type: "message" }, (e) => e.content.length)
272
+ * .otherwise(() => -1); // -1
273
+ * ```
274
+ */
115
275
  otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R;
116
276
  }
117
- /**
118
- * Result-locked variant of {@link MatchBuilder}. Constructed via
119
- * `match(...).returnType<R>()`. Now a thin type alias over `MatchBuilder`
120
- * with the `Locked` phantom flag set — kept for backwards compatibility.
121
- */
122
- type LockedMatchBuilder<T, R, HasInput extends boolean = false, Handled extends readonly unknown[] = []> = MatchBuilder<T, R, HasInput, Handled, true>;
123
277
  /**
124
278
  * Start a pattern-matching expression. Two call shapes:
125
279
  *
@@ -152,8 +306,24 @@ declare function match<T = never>(): MatchBuilder<T, never, false>;
152
306
  * Accepts plain boolean guards (`(x) => x.length > 0`) or TS type-predicate
153
307
  * guards (`(x): x is Foo => ...`). For type predicates, the matched handler's
154
308
  * input is narrowed to the predicate's target type via {@link Narrow}.
309
+ *
310
+ * Note: a plain boolean guard does not advance exhaustiveness tracking — pair
311
+ * it with `.otherwise()`, or use a type predicate to narrow the union.
312
+ *
313
+ * @param guard - boolean or type-predicate guard function
314
+ * @returns the guard, usable as a {@link Pattern} in `.with`
315
+ *
316
+ * @example
317
+ * ```ts
318
+ * const sign = match<number>()
319
+ * .with(when((n: number) => n < 0), () => "neg")
320
+ * .with(when((n: number) => n >= 0), () => "non-neg")
321
+ * .exhaustive();
322
+ * sign(-1); // "neg"
323
+ * sign(0); // "non-neg"
324
+ * ```
155
325
  */
156
326
  declare function when<T, U extends T>(guard: (input: T) => input is U): (input: T) => input is U;
157
327
  declare function when<T>(guard: (input: T) => boolean): Pattern<T>;
158
328
 
159
- export { type ExhaustMatched, type ExhaustiveResult, type HandledUnion, type IsExhaustive, type LockedMatchBuilder, MatchBuilder, type Narrow, type NarrowUnion, type NonExhaustiveError, type Pattern, type RemainingCases, assertNever, match, when };
329
+ export { type ExhaustMatched, type ExhaustiveResult, type HandledUnion, type IsExhaustive, MatchBuilder, type Narrow, type NarrowUnion, type NonExhaustiveError, type Pattern, type RemainingCases, assertNever, match, when };
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
- /** One handler for several patterns (OR). Handler input is the union of narrowed members. */
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
- /** `withOneOf` for exactly two patterns. */
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
- /** Run on the value passed to {@link match}, or on `input` when curried. */
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":"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","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 * 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: number) => n < 0), () => \"neg\")\n * .with(when((n: number) => 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":";;;AASO,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 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"]}
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 unions.
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 unions.
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":";AASO,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 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"]}
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.1.4",
3
+ "version": "0.3.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.0",
65
+ "@onrails/result": "0.2.0",
66
66
  "ts-expect": "^1.3.0",
67
67
  "tsup": "^8.5.1"
68
68
  }