@onrails/pattern 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -28,6 +28,7 @@ var matches = (input, pattern) => {
28
28
  return true;
29
29
  };
30
30
  var NO_MATCH = /* @__PURE__ */ Symbol("@onrails/pattern/no-match");
31
+ var NO_INPUT = /* @__PURE__ */ Symbol("@onrails/pattern/no-input");
31
32
  var runCases = (input, cases) => {
32
33
  for (const c of cases) {
33
34
  if (c.test(input)) {
@@ -41,7 +42,7 @@ var caseForPatterns = (patterns, handler) => ({
41
42
  run: handler
42
43
  });
43
44
  var MatchBuilder = class _MatchBuilder {
44
- constructor(cases = [], input, _handled = []) {
45
+ constructor(cases, input, _handled = []) {
45
46
  this.cases = cases;
46
47
  this.input = input;
47
48
  this._handled = _handled;
@@ -49,21 +50,26 @@ var MatchBuilder = class _MatchBuilder {
49
50
  cases;
50
51
  input;
51
52
  _handled;
53
+ /** Appends a runtime case plus a phantom `H` entry on the handled tuple. */
54
+ addCase(next) {
55
+ return new _MatchBuilder(
56
+ [...this.cases, next],
57
+ this.input,
58
+ // Safe: _handled is a phantom tuple — only its type-level contents matter.
59
+ this._handled
60
+ );
61
+ }
52
62
  with(pattern, handler) {
53
- const next = {
63
+ return this.addCase({
54
64
  test: (input) => matches(input, pattern),
55
65
  run: handler
56
- };
57
- return new _MatchBuilder([...this.cases, next], this.input, [
58
- ...this._handled
59
- ]);
66
+ });
60
67
  }
61
68
  /** One handler for several patterns (OR). Handler input is the union of narrowed members. */
62
69
  withOneOf(patterns, handler) {
63
- const next = caseForPatterns(patterns, handler);
64
- return new _MatchBuilder([...this.cases, next], this.input, [
65
- ...this._handled
66
- ]);
70
+ return this.addCase(
71
+ caseForPatterns(patterns, handler)
72
+ );
67
73
  }
68
74
  /** `withOneOf` for exactly two patterns. */
69
75
  withEither(pattern1, pattern2, handler) {
@@ -73,14 +79,28 @@ var MatchBuilder = class _MatchBuilder {
73
79
  * Lock the result type. All subsequent `.with()` handlers must return `R2`.
74
80
  * Useful when branch return-type inference widens to a union narrower than
75
81
  * the slot the match feeds into (e.g. `ReactNode`).
82
+ *
83
+ * Also the fix for literal-widening: when a handler returns an object whose
84
+ * field is a union literal, inference widens it (e.g. `{ mode: "native" }`
85
+ * becomes `{ mode: string }`, breaking a `Mode` union). Lock the target type
86
+ * instead of sprinkling `as const`:
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * type State = { mode: "compat" | "native" };
91
+ * const next = match(flag)
92
+ * .returnType<State>() // handlers now checked against State
93
+ * .with("--native", () => ({ mode: "native" })) // no `as const` needed
94
+ * .otherwise(() => state);
95
+ * ```
76
96
  */
77
97
  returnType() {
78
98
  return new _MatchBuilder(this.cases, this.input, this._handled);
79
99
  }
80
100
  /** Run on the value passed to {@link match}, or on `input` when curried. */
81
- run(input) {
82
- const value = input ?? this.input;
83
- if (value === void 0) {
101
+ run(...input) {
102
+ const value = input.length === 1 ? input[0] : this.input;
103
+ if (value === NO_INPUT) {
84
104
  throw new Error("match.run: no input value");
85
105
  }
86
106
  const out = runCases(value, this.cases);
@@ -90,24 +110,18 @@ var MatchBuilder = class _MatchBuilder {
90
110
  return out;
91
111
  }
92
112
  exhaustive() {
93
- if (this.input !== void 0) {
94
- return this.run();
95
- }
96
- return ((value) => this.run(value));
113
+ return this.input === NO_INPUT ? (value) => this.run(value) : this.run();
97
114
  }
98
115
  otherwise(handler) {
99
116
  const runWithFallback = (value) => {
100
117
  const out = runCases(value, this.cases);
101
118
  return out === NO_MATCH ? handler(value) : out;
102
119
  };
103
- if (this.input !== void 0) {
104
- return runWithFallback(this.input);
105
- }
106
- return runWithFallback;
120
+ return this.input === NO_INPUT ? runWithFallback : runWithFallback(this.input);
107
121
  }
108
122
  };
109
- function match(input) {
110
- return new MatchBuilder([], input);
123
+ function match(...input) {
124
+ return input.length === 1 ? new MatchBuilder([], input[0]) : new MatchBuilder([], NO_INPUT);
111
125
  }
112
126
 
113
127
  // src/tag.ts
@@ -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\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 private readonly input?: T,\n // Safe: phantom tuple — only used for compile-time exhaustiveness tracking.\n readonly _handled: Handled = [] as unknown as Handled,\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 const next: Case<T, unknown> = {\n test: (input) => matches(input, pattern),\n run: handler as (input: T) => unknown,\n };\n return new MatchBuilder([...this.cases, next], this.input, [\n ...this._handled,\n ] as unknown as readonly [...Handled, ExhaustMatched<T, P>]);\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 const next = caseForPatterns(patterns, handler as (input: T) => unknown);\n return new MatchBuilder([...this.cases, next], this.input, [\n ...this._handled,\n ] as unknown as readonly [...Handled, NarrowUnion<T, Ps>]);\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 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?: T): R {\n const value = input ?? this.input;\n if (value === undefined) {\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 if (this.input !== undefined) {\n return this.run() as ExhaustiveResult<T, Handled, R, HasInput>;\n }\n return ((value: T) => this.run(value)) 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 if (this.input !== undefined) {\n return runWithFallback(this.input) as HasInput extends true ? R : (input: T) => R;\n }\n return runWithFallback 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>(input?: T): MatchBuilder<T, never, boolean> {\n return new MatchBuilder([], 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":["/** 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"]}
package/dist/index.d.cts CHANGED
@@ -3,12 +3,18 @@ export { matchTag } from './tag.cjs';
3
3
  /** Use in `default` branches after manual narrowing — compile-time exhaustiveness check. */
4
4
  declare const assertNever: (value: never, message?: string) => never;
5
5
 
6
- type GuardTarget$1<F> = F extends (input: any) => input is infer U ? U : never;
6
+ type GuardTarget<F> = F extends (input: any) => input is infer U ? U : never;
7
+ type NarrowObject<T, P> = [Extract<T, P>] extends [never] ? T & P : Extract<T, P>;
8
+ /** Union of members narrowed by each pattern in `Ps` (multi-branch handlers). */
9
+ type NarrowUnion<T, Ps extends readonly unknown[]> = Narrow<T, Ps[number]>;
10
+ /** Narrows `input` when `pattern` is a shallow object, literal discriminant, or type predicate. */
11
+ type Narrow<T, P> = P extends (input: T) => boolean ? [GuardTarget<P>] extends [never] ? T : NarrowObject<T, GuardTarget<P>> : P extends Record<string, unknown> ? NarrowObject<T, P> : T extends P ? Extract<T, P> : T;
12
+
7
13
  /**
8
14
  * Union members of `T` ruled out by pattern `P`.
9
15
  * Boolean guards (non-predicates) do not advance exhaustiveness — use `when(isX)` or `.otherwise()`.
10
16
  */
11
- type ExhaustMatched<T, P> = P extends (input: T) => boolean ? [GuardTarget$1<P>] extends [never] ? never : Extract<T, GuardTarget$1<P>> : Extract<T, P>;
17
+ type ExhaustMatched<T, P> = P extends (input: T) => boolean ? [GuardTarget<P>] extends [never] ? never : Extract<T, GuardTarget<P>> : Extract<T, P>;
12
18
  /** Union of cases already handled by prior branches. */
13
19
  type HandledUnion<Handled extends readonly unknown[]> = Handled[number];
14
20
  /** Input cases not yet covered by `.with` / `.withOneOf` / `.withEither`. */
@@ -25,13 +31,6 @@ type NonExhaustiveError<Remaining> = {
25
31
  /** `.exhaustive()` return type — `NonExhaustiveError` when cases are missing. */
26
32
  type ExhaustiveResult<T, Handled extends readonly unknown[], R, HasInput extends boolean> = IsExhaustive<T, Handled> extends true ? ExhaustiveOutput<R, HasInput, T> : NonExhaustiveError<RemainingCases<T, Handled>>;
27
33
 
28
- type GuardTarget<F> = F extends (input: any) => input is infer U ? U : never;
29
- type NarrowObject<T, P> = [Extract<T, P>] extends [never] ? T & P : Extract<T, P>;
30
- /** Union of members narrowed by each pattern in `Ps` (multi-branch handlers). */
31
- type NarrowUnion<T, Ps extends readonly unknown[]> = Narrow<T, Ps[number]>;
32
- /** Narrows `input` when `pattern` is a shallow object, literal discriminant, or type predicate. */
33
- type Narrow<T, P> = P extends (input: T) => boolean ? [GuardTarget<P>] extends [never] ? T : NarrowObject<T, GuardTarget<P>> : P extends Record<string, unknown> ? NarrowObject<T, P> : T extends P ? Extract<T, P> : T;
34
-
35
34
  /**
36
35
  * A pattern that matches a value of type `T`. One of:
37
36
  *
@@ -57,6 +56,8 @@ type Case<T, R> = {
57
56
  readonly test: (input: T) => boolean;
58
57
  readonly run: (input: T) => R;
59
58
  };
59
+ declare const NO_INPUT: unique symbol;
60
+ type NoInput = typeof NO_INPUT;
60
61
  type NextResult<Locked extends boolean, R, R2> = Locked extends true ? R : R | R2;
61
62
  type HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2;
62
63
  /**
@@ -78,9 +79,11 @@ type HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2
78
79
  */
79
80
  declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handled extends readonly unknown[] = [], Locked extends boolean = false> {
80
81
  private readonly cases;
81
- private readonly input?;
82
+ private readonly input;
82
83
  readonly _handled: Handled;
83
- constructor(cases?: readonly Case<T, unknown>[], input?: T | undefined, _handled?: Handled);
84
+ constructor(cases: readonly Case<T, unknown>[], input: T | NoInput, _handled?: Handled);
85
+ /** Appends a runtime case plus a phantom `H` entry on the handled tuple. */
86
+ private addCase;
84
87
  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>;
85
88
  /** One handler for several patterns (OR). Handler input is the union of narrowed members. */
86
89
  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,10 +93,24 @@ declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handl
90
93
  * Lock the result type. All subsequent `.with()` handlers must return `R2`.
91
94
  * Useful when branch return-type inference widens to a union narrower than
92
95
  * the slot the match feeds into (e.g. `ReactNode`).
96
+ *
97
+ * Also the fix for literal-widening: when a handler returns an object whose
98
+ * field is a union literal, inference widens it (e.g. `{ mode: "native" }`
99
+ * becomes `{ mode: string }`, breaking a `Mode` union). Lock the target type
100
+ * instead of sprinkling `as const`:
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * type State = { mode: "compat" | "native" };
105
+ * const next = match(flag)
106
+ * .returnType<State>() // handlers now checked against State
107
+ * .with("--native", () => ({ mode: "native" })) // no `as const` needed
108
+ * .otherwise(() => state);
109
+ * ```
93
110
  */
94
111
  returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true>;
95
112
  /** Run on the value passed to {@link match}, or on `input` when curried. */
96
- run(input?: T): R;
113
+ run(...input: readonly [input: T] | readonly []): R;
97
114
  exhaustive(): ExhaustiveResult<T, Handled, R, HasInput>;
98
115
  otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R;
99
116
  }
package/dist/index.d.ts CHANGED
@@ -3,12 +3,18 @@ export { matchTag } from './tag.js';
3
3
  /** Use in `default` branches after manual narrowing — compile-time exhaustiveness check. */
4
4
  declare const assertNever: (value: never, message?: string) => never;
5
5
 
6
- type GuardTarget$1<F> = F extends (input: any) => input is infer U ? U : never;
6
+ type GuardTarget<F> = F extends (input: any) => input is infer U ? U : never;
7
+ type NarrowObject<T, P> = [Extract<T, P>] extends [never] ? T & P : Extract<T, P>;
8
+ /** Union of members narrowed by each pattern in `Ps` (multi-branch handlers). */
9
+ type NarrowUnion<T, Ps extends readonly unknown[]> = Narrow<T, Ps[number]>;
10
+ /** Narrows `input` when `pattern` is a shallow object, literal discriminant, or type predicate. */
11
+ type Narrow<T, P> = P extends (input: T) => boolean ? [GuardTarget<P>] extends [never] ? T : NarrowObject<T, GuardTarget<P>> : P extends Record<string, unknown> ? NarrowObject<T, P> : T extends P ? Extract<T, P> : T;
12
+
7
13
  /**
8
14
  * Union members of `T` ruled out by pattern `P`.
9
15
  * Boolean guards (non-predicates) do not advance exhaustiveness — use `when(isX)` or `.otherwise()`.
10
16
  */
11
- type ExhaustMatched<T, P> = P extends (input: T) => boolean ? [GuardTarget$1<P>] extends [never] ? never : Extract<T, GuardTarget$1<P>> : Extract<T, P>;
17
+ type ExhaustMatched<T, P> = P extends (input: T) => boolean ? [GuardTarget<P>] extends [never] ? never : Extract<T, GuardTarget<P>> : Extract<T, P>;
12
18
  /** Union of cases already handled by prior branches. */
13
19
  type HandledUnion<Handled extends readonly unknown[]> = Handled[number];
14
20
  /** Input cases not yet covered by `.with` / `.withOneOf` / `.withEither`. */
@@ -25,13 +31,6 @@ type NonExhaustiveError<Remaining> = {
25
31
  /** `.exhaustive()` return type — `NonExhaustiveError` when cases are missing. */
26
32
  type ExhaustiveResult<T, Handled extends readonly unknown[], R, HasInput extends boolean> = IsExhaustive<T, Handled> extends true ? ExhaustiveOutput<R, HasInput, T> : NonExhaustiveError<RemainingCases<T, Handled>>;
27
33
 
28
- type GuardTarget<F> = F extends (input: any) => input is infer U ? U : never;
29
- type NarrowObject<T, P> = [Extract<T, P>] extends [never] ? T & P : Extract<T, P>;
30
- /** Union of members narrowed by each pattern in `Ps` (multi-branch handlers). */
31
- type NarrowUnion<T, Ps extends readonly unknown[]> = Narrow<T, Ps[number]>;
32
- /** Narrows `input` when `pattern` is a shallow object, literal discriminant, or type predicate. */
33
- type Narrow<T, P> = P extends (input: T) => boolean ? [GuardTarget<P>] extends [never] ? T : NarrowObject<T, GuardTarget<P>> : P extends Record<string, unknown> ? NarrowObject<T, P> : T extends P ? Extract<T, P> : T;
34
-
35
34
  /**
36
35
  * A pattern that matches a value of type `T`. One of:
37
36
  *
@@ -57,6 +56,8 @@ type Case<T, R> = {
57
56
  readonly test: (input: T) => boolean;
58
57
  readonly run: (input: T) => R;
59
58
  };
59
+ declare const NO_INPUT: unique symbol;
60
+ type NoInput = typeof NO_INPUT;
60
61
  type NextResult<Locked extends boolean, R, R2> = Locked extends true ? R : R | R2;
61
62
  type HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2;
62
63
  /**
@@ -78,9 +79,11 @@ type HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2
78
79
  */
79
80
  declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handled extends readonly unknown[] = [], Locked extends boolean = false> {
80
81
  private readonly cases;
81
- private readonly input?;
82
+ private readonly input;
82
83
  readonly _handled: Handled;
83
- constructor(cases?: readonly Case<T, unknown>[], input?: T | undefined, _handled?: Handled);
84
+ constructor(cases: readonly Case<T, unknown>[], input: T | NoInput, _handled?: Handled);
85
+ /** Appends a runtime case plus a phantom `H` entry on the handled tuple. */
86
+ private addCase;
84
87
  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>;
85
88
  /** One handler for several patterns (OR). Handler input is the union of narrowed members. */
86
89
  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,10 +93,24 @@ declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handl
90
93
  * Lock the result type. All subsequent `.with()` handlers must return `R2`.
91
94
  * Useful when branch return-type inference widens to a union narrower than
92
95
  * the slot the match feeds into (e.g. `ReactNode`).
96
+ *
97
+ * Also the fix for literal-widening: when a handler returns an object whose
98
+ * field is a union literal, inference widens it (e.g. `{ mode: "native" }`
99
+ * becomes `{ mode: string }`, breaking a `Mode` union). Lock the target type
100
+ * instead of sprinkling `as const`:
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * type State = { mode: "compat" | "native" };
105
+ * const next = match(flag)
106
+ * .returnType<State>() // handlers now checked against State
107
+ * .with("--native", () => ({ mode: "native" })) // no `as const` needed
108
+ * .otherwise(() => state);
109
+ * ```
93
110
  */
94
111
  returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true>;
95
112
  /** Run on the value passed to {@link match}, or on `input` when curried. */
96
- run(input?: T): R;
113
+ run(...input: readonly [input: T] | readonly []): R;
97
114
  exhaustive(): ExhaustiveResult<T, Handled, R, HasInput>;
98
115
  otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R;
99
116
  }
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ var matches = (input, pattern) => {
26
26
  return true;
27
27
  };
28
28
  var NO_MATCH = /* @__PURE__ */ Symbol("@onrails/pattern/no-match");
29
+ var NO_INPUT = /* @__PURE__ */ Symbol("@onrails/pattern/no-input");
29
30
  var runCases = (input, cases) => {
30
31
  for (const c of cases) {
31
32
  if (c.test(input)) {
@@ -39,7 +40,7 @@ var caseForPatterns = (patterns, handler) => ({
39
40
  run: handler
40
41
  });
41
42
  var MatchBuilder = class _MatchBuilder {
42
- constructor(cases = [], input, _handled = []) {
43
+ constructor(cases, input, _handled = []) {
43
44
  this.cases = cases;
44
45
  this.input = input;
45
46
  this._handled = _handled;
@@ -47,21 +48,26 @@ var MatchBuilder = class _MatchBuilder {
47
48
  cases;
48
49
  input;
49
50
  _handled;
51
+ /** Appends a runtime case plus a phantom `H` entry on the handled tuple. */
52
+ addCase(next) {
53
+ return new _MatchBuilder(
54
+ [...this.cases, next],
55
+ this.input,
56
+ // Safe: _handled is a phantom tuple — only its type-level contents matter.
57
+ this._handled
58
+ );
59
+ }
50
60
  with(pattern, handler) {
51
- const next = {
61
+ return this.addCase({
52
62
  test: (input) => matches(input, pattern),
53
63
  run: handler
54
- };
55
- return new _MatchBuilder([...this.cases, next], this.input, [
56
- ...this._handled
57
- ]);
64
+ });
58
65
  }
59
66
  /** One handler for several patterns (OR). Handler input is the union of narrowed members. */
60
67
  withOneOf(patterns, handler) {
61
- const next = caseForPatterns(patterns, handler);
62
- return new _MatchBuilder([...this.cases, next], this.input, [
63
- ...this._handled
64
- ]);
68
+ return this.addCase(
69
+ caseForPatterns(patterns, handler)
70
+ );
65
71
  }
66
72
  /** `withOneOf` for exactly two patterns. */
67
73
  withEither(pattern1, pattern2, handler) {
@@ -71,14 +77,28 @@ var MatchBuilder = class _MatchBuilder {
71
77
  * Lock the result type. All subsequent `.with()` handlers must return `R2`.
72
78
  * Useful when branch return-type inference widens to a union narrower than
73
79
  * the slot the match feeds into (e.g. `ReactNode`).
80
+ *
81
+ * Also the fix for literal-widening: when a handler returns an object whose
82
+ * field is a union literal, inference widens it (e.g. `{ mode: "native" }`
83
+ * becomes `{ mode: string }`, breaking a `Mode` union). Lock the target type
84
+ * instead of sprinkling `as const`:
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * type State = { mode: "compat" | "native" };
89
+ * const next = match(flag)
90
+ * .returnType<State>() // handlers now checked against State
91
+ * .with("--native", () => ({ mode: "native" })) // no `as const` needed
92
+ * .otherwise(() => state);
93
+ * ```
74
94
  */
75
95
  returnType() {
76
96
  return new _MatchBuilder(this.cases, this.input, this._handled);
77
97
  }
78
98
  /** Run on the value passed to {@link match}, or on `input` when curried. */
79
- run(input) {
80
- const value = input ?? this.input;
81
- if (value === void 0) {
99
+ run(...input) {
100
+ const value = input.length === 1 ? input[0] : this.input;
101
+ if (value === NO_INPUT) {
82
102
  throw new Error("match.run: no input value");
83
103
  }
84
104
  const out = runCases(value, this.cases);
@@ -88,24 +108,18 @@ var MatchBuilder = class _MatchBuilder {
88
108
  return out;
89
109
  }
90
110
  exhaustive() {
91
- if (this.input !== void 0) {
92
- return this.run();
93
- }
94
- return ((value) => this.run(value));
111
+ return this.input === NO_INPUT ? (value) => this.run(value) : this.run();
95
112
  }
96
113
  otherwise(handler) {
97
114
  const runWithFallback = (value) => {
98
115
  const out = runCases(value, this.cases);
99
116
  return out === NO_MATCH ? handler(value) : out;
100
117
  };
101
- if (this.input !== void 0) {
102
- return runWithFallback(this.input);
103
- }
104
- return runWithFallback;
118
+ return this.input === NO_INPUT ? runWithFallback : runWithFallback(this.input);
105
119
  }
106
120
  };
107
- function match(input) {
108
- return new MatchBuilder([], input);
121
+ function match(...input) {
122
+ return input.length === 1 ? new MatchBuilder([], input[0]) : new MatchBuilder([], NO_INPUT);
109
123
  }
110
124
 
111
125
  // src/tag.ts
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":";AACO,IAAM,WAAA,GAAc,CAAC,KAAA,EAAc,OAAA,GAAU,aAAA,KAAyB;AAC3E,EAAA,MAAM,IAAI,MAAM,CAAA,EAAG,OAAO,KAAK,MAAA,CAAO,KAAK,CAAC,CAAA,CAAE,CAAA;AAChD;;;ACgCA,IAAM,OAAA,GAAU,CAAI,OAAA,KAClB,OAAO,OAAA,KAAY,UAAA;AAErB,IAAM,kBAAkB,CAAC,OAAA,KACvB,OAAO,OAAA,KAAY,YAAY,OAAA,KAAY,IAAA;AAE7C,IAAM,OAAA,GAAU,CAAI,KAAA,EAAU,OAAA,KAAiC;AAC7D,EAAA,IAAI,OAAA,CAAQ,OAAO,CAAA,EAAG;AACpB,IAAA,OAAO,QAAQ,KAAK,CAAA;AAAA,EACtB;AACA,EAAA,IAAI,CAAC,eAAA,CAAgB,OAAO,CAAA,EAAG;AAC7B,IAAA,OAAO,KAAA,KAAU,OAAA;AAAA,EACnB;AACA,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,KAAU,IAAA,EAAM;AAC/C,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,MAAM,MAAA,GAAS,KAAA;AACf,EAAA,MAAM,aAAA,GAAgB,OAAA;AACtB,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,aAAa,CAAA,EAAG;AAC5C,IAAA,IAAI,MAAA,CAAO,GAAG,CAAA,KAAM,aAAA,CAAc,GAAG,CAAA,EAAG;AACtC,MAAA,OAAO,KAAA;AAAA,IACT;AAAA,EACF;AACA,EAAA,OAAO,IAAA;AACT,CAAA;AAKA,IAAM,QAAA,0BAAkB,2BAA2B,CAAA;AAGnD,IAAM,QAAA,GAAW,CAAO,KAAA,EAAU,KAAA,KAA8C;AAC9E,EAAA,KAAA,MAAW,KAAK,KAAA,EAAO;AACrB,IAAA,IAAI,CAAA,CAAE,IAAA,CAAK,KAAK,CAAA,EAAG;AACjB,MAAA,OAAO,CAAA,CAAE,IAAI,KAAK,CAAA;AAAA,IACpB;AAAA,EACF;AACA,EAAA,OAAO,QAAA;AACT,CAAA;AAEA,IAAM,eAAA,GAAkB,CACtB,QAAA,EACA,OAAA,MACgB;AAAA,EAChB,IAAA,EAAM,CAAC,KAAA,KAAU,QAAA,CAAS,IAAA,CAAK,CAAC,OAAA,KAAY,OAAA,CAAQ,KAAA,EAAO,OAAO,CAAC,CAAA;AAAA,EACnE,GAAA,EAAK;AACP,CAAA,CAAA;AA2BO,IAAM,YAAA,GAAN,MAAM,aAAA,CAMX;AAAA,EACA,YACmB,KAAA,GAAqC,IACrC,KAAA,EAER,QAAA,GAAoB,EAAC,EAC9B;AAJiB,IAAA,IAAA,CAAA,KAAA,GAAA,KAAA;AACA,IAAA,IAAA,CAAA,KAAA,GAAA,KAAA;AAER,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AAAA,EACR;AAAA,EAJgB,KAAA;AAAA,EACA,KAAA;AAAA,EAER,QAAA;AAAA,EAGX,IAAA,CACE,SACA,OAAA,EAOA;AACA,IAAA,MAAM,IAAA,GAAyB;AAAA,MAC7B,IAAA,EAAM,CAAC,KAAA,KAAU,OAAA,CAAQ,OAAO,OAAO,CAAA;AAAA,MACvC,GAAA,EAAK;AAAA,KACP;AACA,IAAA,OAAO,IAAI,cAAa,CAAC,GAAG,KAAK,KAAA,EAAO,IAAI,CAAA,EAAG,IAAA,CAAK,KAAA,EAAO;AAAA,MACzD,GAAG,IAAA,CAAK;AAAA,KACiD,CAAA;AAAA,EAC7D;AAAA;AAAA,EAGA,SAAA,CACE,UACA,OAAA,EAOA;AACA,IAAA,MAAM,IAAA,GAAO,eAAA,CAAgB,QAAA,EAAU,OAAgC,CAAA;AACvE,IAAA,OAAO,IAAI,cAAa,CAAC,GAAG,KAAK,KAAA,EAAO,IAAI,CAAA,EAAG,IAAA,CAAK,KAAA,EAAO;AAAA,MACzD,GAAG,IAAA,CAAK;AAAA,KAC+C,CAAA;AAAA,EAC3D;AAAA;AAAA,EAGA,UAAA,CACE,QAAA,EACA,QAAA,EACA,OAAA,EAOA;AACA,IAAA,OAAO,KAAK,SAAA,CAAU,CAAC,QAAA,EAAU,QAAQ,GAAG,OAAO,CAAA;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,UAAA,GAA+D;AAC7D,IAAA,OAAO,IAAI,aAAA,CAA6C,IAAA,CAAK,OAAO,IAAA,CAAK,KAAA,EAAO,KAAK,QAAQ,CAAA;AAAA,EAC/F;AAAA;AAAA,EAGA,IAAI,KAAA,EAAc;AAChB,IAAA,MAAM,KAAA,GAAQ,SAAS,IAAA,CAAK,KAAA;AAC5B,IAAA,IAAI,UAAU,MAAA,EAAW;AACvB,MAAA,MAAM,IAAI,MAAM,2BAA2B,CAAA;AAAA,IAC7C;AACA,IAAA,MAAM,GAAA,GAAM,QAAA,CAAS,KAAA,EAAO,IAAA,CAAK,KAAK,CAAA;AACtC,IAAA,IAAI,QAAQ,QAAA,EAAU;AACpB,MAAA,MAAM,IAAI,MAAM,6CAA6C,CAAA;AAAA,IAC/D;AACA,IAAA,OAAO,GAAA;AAAA,EACT;AAAA,EAEA,UAAA,GAAwD;AACtD,IAAA,IAAI,IAAA,CAAK,UAAU,MAAA,EAAW;AAC5B,MAAA,OAAO,KAAK,GAAA,EAAI;AAAA,IAClB;AACA,IAAA,QAAQ,CAAC,KAAA,KAAa,IAAA,CAAK,GAAA,CAAI,KAAK,CAAA;AAAA,EACtC;AAAA,EAEA,UAAU,OAAA,EAAuE;AAC/E,IAAA,MAAM,eAAA,GAAkB,CAAC,KAAA,KAAgB;AACvC,MAAA,MAAM,GAAA,GAAM,QAAA,CAAS,KAAA,EAAO,IAAA,CAAK,KAAK,CAAA;AACtC,MAAA,OAAO,GAAA,KAAQ,QAAA,GAAW,OAAA,CAAQ,KAAK,CAAA,GAAK,GAAA;AAAA,IAC9C,CAAA;AACA,IAAA,IAAI,IAAA,CAAK,UAAU,MAAA,EAAW;AAC5B,MAAA,OAAO,eAAA,CAAgB,KAAK,KAAK,CAAA;AAAA,IACnC;AACA,IAAA,OAAO,eAAA;AAAA,EACT;AACF;AAuCO,SAAS,MAAS,KAAA,EAA4C;AACnE,EAAA,OAAO,IAAI,YAAA,CAAa,EAAC,EAAG,KAAK,CAAA;AACnC;;;ACrPO,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;;;ACFO,SAAS,KAAQ,KAAA,EAA0C;AAChE,EAAA,OAAO,KAAA;AACT","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\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 private readonly input?: T,\n // Safe: phantom tuple — only used for compile-time exhaustiveness tracking.\n readonly _handled: Handled = [] as unknown as Handled,\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 const next: Case<T, unknown> = {\n test: (input) => matches(input, pattern),\n run: handler as (input: T) => unknown,\n };\n return new MatchBuilder([...this.cases, next], this.input, [\n ...this._handled,\n ] as unknown as readonly [...Handled, ExhaustMatched<T, P>]);\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 const next = caseForPatterns(patterns, handler as (input: T) => unknown);\n return new MatchBuilder([...this.cases, next], this.input, [\n ...this._handled,\n ] as unknown as readonly [...Handled, NarrowUnion<T, Ps>]);\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 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?: T): R {\n const value = input ?? this.input;\n if (value === undefined) {\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 if (this.input !== undefined) {\n return this.run() as ExhaustiveResult<T, Handled, R, HasInput>;\n }\n return ((value: T) => this.run(value)) 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 if (this.input !== undefined) {\n return runWithFallback(this.input) as HasInput extends true ? R : (input: T) => R;\n }\n return runWithFallback 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>(input?: T): MatchBuilder<T, never, boolean> {\n return new MatchBuilder([], 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":["/** 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onrails/pattern",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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",