@onrails/pattern 0.1.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 ADDED
@@ -0,0 +1,71 @@
1
+ # @onrails/pattern — design
2
+
3
+ Lightweight matching for **owned** tagged unions and finite domain states. Inspired by ts-pattern; aligned with `@onrails/result` DX.
4
+
5
+ ## Goals
6
+
7
+ - **Smaller runtime** than ts-pattern — shallow object match, literal/primitive equality, guards
8
+ - **ts-pattern-shaped API**: `match(x).with(...).exhaustive()` / `.otherwise()`
9
+ - **`matchTag`** for `_tag` unions (`Ok` / `Err`, `Some` / `None`, domain ADTs)
10
+ - No `P.*` wildcard zoo in v1 — use `when(guard)` or `matchTag`
11
+
12
+ ## Handler narrowing
13
+
14
+ `.with(pattern, handler)` narrows the handler's input via `Narrow<T, P>`:
15
+
16
+ - **Discriminated union**: `Extract<T, P>` picks the matching member.
17
+ - **Single object type**: falls back to intersection `T & P` so structural matches still narrow (e.g. `{ status: "failed" }` on a `Job` with `status: JobStatus`).
18
+ - **Type-predicate guard** (`(x): x is U`): narrows to `U` via `when(predicate)`.
19
+ - **Plain boolean guard** (`(x) => boolean`): leaves the handler input as `T`.
20
+
21
+ `Pattern<T>` admits object-shaped patterns only when `T extends object`. Primitive `T` (`number | string`) accepts literals and guards but not free-form objects — this prevents `Record<string, unknown>` distributing into `Narrow` and polluting the handler input type.
22
+
23
+ ## Result-type seeding
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.).
26
+
27
+ ## Type tests
28
+
29
+ `test/types.spec.ts` uses `ts-expect` (`expectType` + `TypeEqual`) — same approach as styled-cva.
30
+
31
+ ## Compile-time exhaustiveness
32
+
33
+ `.exhaustive()` is only typed when every member of union `T` is covered by prior `.with` / `.withOneOf` / `.withEither` branches. The builder tracks `Matched`; `RemainingCases<T, Matched>` must be `never`.
34
+
35
+ - Object/literal patterns use `Extract<T, P>`.
36
+ - `when(predicate)` with a **type predicate** narrows like `.with`; plain boolean guards do **not** advance exhaustiveness (use `.otherwise()` or add explicit branches).
37
+ - **Single object types** with an enum/status field (not a top-level union) are not proven exhaustive — model as a discriminated union or use `.otherwise()`.
38
+
39
+ Runtime still throws if a value slips through (e.g. unsound cast on input).
40
+
41
+ ## Non-goals (v1)
42
+
43
+ - Deep/spread patterns, `P.select`, `P.not`, nested unwrapping
44
+ - Replacing `if` for two-branch checks or nullable guards
45
+
46
+ ## Exports
47
+
48
+ | Subpath | Purpose |
49
+ |---------|---------|
50
+ | `@onrails/pattern` | `match`, `when`, `assertNever`, `MatchBuilder`, `LockedMatchBuilder`, `Pattern`, `Narrow`, `NarrowUnion`, `RemainingCases`, `IsExhaustive`, `NonExhaustiveError` |
51
+ | `@onrails/pattern/tag` | `matchTag` for `_tag` dispatch |
52
+
53
+ ## Migration from ts-pattern
54
+
55
+ | ts-pattern | @onrails/pattern |
56
+ |------------|------------------|
57
+ | `match(x).with({ type: "a" }, fn).exhaustive()` | Same |
58
+ | `match(x).with("a", fn).exhaustive()` | Same (primitive / literal union) |
59
+ | `match(x).with(p1, p2, fn)` (multi-pattern) | `.withOneOf([p1, p2], fn)` or `.withEither(p1, p2, fn)` |
60
+ | `P.when(fn)` | `when(fn)` (preserves type-predicate narrowing) |
61
+ | `match(x).returnType<R>()` | Same |
62
+ | `P._` / nested selects | Not v1 — keep ts-pattern or refactor to `matchTag` |
63
+
64
+ ## Multi-pattern
65
+
66
+ `.withOneOf([p1, p2, …], handler)` registers one case whose test ORs the patterns. Handler input is `NarrowUnion<T, Ps>` (union of per-pattern narrowings). `.withEither(p1, p2, handler)` is sugar for two patterns.
67
+
68
+ ## Deferred
69
+
70
+ - Variadic `.with(p1, p2, fn)` overload (ts-pattern arity style)
71
+ - `compat/ts-pattern` re-export or codemod
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alan R. Soares
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # @onrails/pattern
2
+
3
+ Exhaustive matching for owned tagged unions — ts-pattern-shaped API, smaller runtime than full ts-pattern.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @onrails/pattern
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { match } from "@onrails/pattern";
15
+
16
+ type Status = "idle" | "running" | "done";
17
+
18
+ const label = (s: Status) =>
19
+ match(s)
20
+ .with("idle", () => "Idle")
21
+ .with("running", () => "Running")
22
+ .with("done", () => "Done")
23
+ .exhaustive();
24
+ ```
25
+
26
+ Object discriminant (shallow key match):
27
+
28
+ ```ts
29
+ import { match } from "@onrails/pattern";
30
+
31
+ const render = (event: { type: "msg"; text: string } | { type: "err"; code: number }) =>
32
+ match(event)
33
+ .with({ type: "msg" }, (e) => e.text) // `e` is narrowed — no cast
34
+ .with({ type: "err" }, (e) => String(e.code))
35
+ .exhaustive();
36
+ ```
37
+
38
+ Type-predicate guards via `when`:
39
+
40
+ ```ts
41
+ import { match, when } from "@onrails/pattern";
42
+
43
+ type Event = { type: "msg"; text: string } | { type: "err"; code: number };
44
+ const isMsg = (e: Event): e is Extract<Event, { type: "msg" }> => e.type === "msg";
45
+
46
+ const render = (e: Event) =>
47
+ match(e)
48
+ .with(when(isMsg), (msg) => msg.text) // `msg` narrowed to { type: "msg"; text: string }
49
+ .otherwise(() => "");
50
+ ```
51
+
52
+ Lock the result type with `returnType<R>()` when branch inference would otherwise widen too narrowly:
53
+
54
+ ```ts
55
+ import type { ReactNode } from "react";
56
+ import { match } from "@onrails/pattern";
57
+
58
+ const render = (p: Part): ReactNode =>
59
+ match(p)
60
+ .returnType<ReactNode>() // every `.with` handler must return ReactNode
61
+ .with({ type: "text" }, (t) => <Text>{t.text}</Text>)
62
+ .with({ type: "image" }, (i) => <Image src={i.src} />)
63
+ .otherwise(() => null);
64
+ ```
65
+
66
+ `_tag` unions (`@onrails/result`, `@onrails/maybe`):
67
+
68
+ ```ts
69
+ import { matchTag } from "@onrails/pattern/tag";
70
+ import { isOk, type Result } from "@onrails/result";
71
+
72
+ const show = <T, E>(r: Result<T, E>) =>
73
+ matchTag(r, {
74
+ Ok: (v) => v.value,
75
+ Err: (e) => e.error,
76
+ });
77
+ ```
78
+
79
+ ## Subpaths
80
+
81
+ | Path | Contents |
82
+ |------|----------|
83
+ | `@onrails/pattern` | `match`, `when`, `assertNever`, `MatchBuilder`, `LockedMatchBuilder` |
84
+ | `@onrails/pattern/tag` | `matchTag` |
85
+
86
+ See [DESIGN.md](./DESIGN.md).
package/dist/index.cjs ADDED
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+
3
+ // src/assert.ts
4
+ var assertNever = (value, message = "Unreachable") => {
5
+ throw new Error(`${message}: ${String(value)}`);
6
+ };
7
+
8
+ // src/match.ts
9
+ var isGuard = (pattern) => typeof pattern === "function";
10
+ var isPatternObject = (pattern) => typeof pattern === "object" && pattern !== null;
11
+ var matches = (input, pattern) => {
12
+ if (isGuard(pattern)) {
13
+ return pattern(input);
14
+ }
15
+ if (!isPatternObject(pattern)) {
16
+ return input === pattern;
17
+ }
18
+ if (typeof input !== "object" || input === null) {
19
+ return false;
20
+ }
21
+ const record = input;
22
+ const patternRecord = pattern;
23
+ for (const key of Object.keys(patternRecord)) {
24
+ if (record[key] !== patternRecord[key]) {
25
+ return false;
26
+ }
27
+ }
28
+ return true;
29
+ };
30
+ var NO_MATCH = /* @__PURE__ */ Symbol("@onrails/pattern/no-match");
31
+ var runCases = (input, cases) => {
32
+ for (const c of cases) {
33
+ if (c.test(input)) {
34
+ return c.run(input);
35
+ }
36
+ }
37
+ return NO_MATCH;
38
+ };
39
+ var caseForPatterns = (patterns, handler) => ({
40
+ test: (input) => patterns.some((pattern) => matches(input, pattern)),
41
+ run: handler
42
+ });
43
+ var MatchBuilder = class _MatchBuilder {
44
+ constructor(cases = [], input, _handled = []) {
45
+ this.cases = cases;
46
+ this.input = input;
47
+ this._handled = _handled;
48
+ }
49
+ cases;
50
+ input;
51
+ _handled;
52
+ with(pattern, handler) {
53
+ const next = {
54
+ test: (input) => matches(input, pattern),
55
+ run: handler
56
+ };
57
+ return new _MatchBuilder([...this.cases, next], this.input, [
58
+ ...this._handled
59
+ ]);
60
+ }
61
+ /** One handler for several patterns (OR). Handler input is the union of narrowed members. */
62
+ withOneOf(patterns, handler) {
63
+ const next = caseForPatterns(patterns, handler);
64
+ return new _MatchBuilder([...this.cases, next], this.input, [
65
+ ...this._handled
66
+ ]);
67
+ }
68
+ /** `withOneOf` for exactly two patterns. */
69
+ withEither(pattern1, pattern2, handler) {
70
+ return this.withOneOf([pattern1, pattern2], handler);
71
+ }
72
+ /**
73
+ * Lock the result type. All subsequent `.with()` handlers must return `R2`.
74
+ * Useful when branch return-type inference widens to a union narrower than
75
+ * the slot the match feeds into (e.g. `ReactNode`).
76
+ */
77
+ returnType() {
78
+ return new _MatchBuilder(this.cases, this.input, this._handled);
79
+ }
80
+ /** 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) {
84
+ throw new Error("match.run: no input value");
85
+ }
86
+ const out = runCases(value, this.cases);
87
+ if (out === NO_MATCH) {
88
+ throw new Error("Non-exhaustive match: no case matched input");
89
+ }
90
+ return out;
91
+ }
92
+ exhaustive() {
93
+ if (this.input !== void 0) {
94
+ return this.run();
95
+ }
96
+ return ((value) => this.run(value));
97
+ }
98
+ otherwise(handler) {
99
+ const runWithFallback = (value) => {
100
+ const out = runCases(value, this.cases);
101
+ return out === NO_MATCH ? handler(value) : out;
102
+ };
103
+ if (this.input !== void 0) {
104
+ return runWithFallback(this.input);
105
+ }
106
+ return runWithFallback;
107
+ }
108
+ };
109
+ function match(input) {
110
+ return new MatchBuilder([], input);
111
+ }
112
+
113
+ // src/tag.ts
114
+ var matchTag = (value, handlers) => {
115
+ const tag = value._tag;
116
+ const handler = handlers[tag];
117
+ return handler(value);
118
+ };
119
+
120
+ // src/when.ts
121
+ function when(guard) {
122
+ return guard;
123
+ }
124
+
125
+ exports.MatchBuilder = MatchBuilder;
126
+ exports.assertNever = assertNever;
127
+ exports.match = match;
128
+ exports.matchTag = matchTag;
129
+ exports.when = when;
130
+ //# sourceMappingURL=index.cjs.map
131
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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"]}
@@ -0,0 +1,142 @@
1
+ export { matchTag } from './tag.cjs';
2
+
3
+ /** Use in `default` branches after manual narrowing — compile-time exhaustiveness check. */
4
+ declare const assertNever: (value: never, message?: string) => never;
5
+
6
+ type GuardTarget$1<F> = F extends (input: any) => input is infer U ? U : never;
7
+ /**
8
+ * Union members of `T` ruled out by pattern `P`.
9
+ * Boolean guards (non-predicates) do not advance exhaustiveness — use `when(isX)` or `.otherwise()`.
10
+ */
11
+ type ExhaustMatched<T, P> = P extends (input: T) => boolean ? [GuardTarget$1<P>] extends [never] ? never : Extract<T, GuardTarget$1<P>> : Extract<T, P>;
12
+ /** Union of cases already handled by prior branches. */
13
+ type HandledUnion<Handled extends readonly unknown[]> = Handled[number];
14
+ /** Input cases not yet covered by `.with` / `.withOneOf` / `.withEither`. */
15
+ type RemainingCases<T, Handled extends readonly unknown[]> = Exclude<T, HandledUnion<Handled>>;
16
+ type IsExhaustive<T, Handled extends readonly unknown[]> = [
17
+ RemainingCases<T, Handled>
18
+ ] extends [never] ? true : false;
19
+ type ExhaustiveOutput<R, HasInput extends boolean, T> = HasInput extends true ? R : (input: T) => R;
20
+ /** Returned by `.exhaustive()` when union cases are still missing (compile-time error). */
21
+ type NonExhaustiveError<Remaining> = {
22
+ readonly __nonExhaustive: "Add .with() branches for remaining cases";
23
+ readonly remaining: Remaining;
24
+ };
25
+ /** `.exhaustive()` return type — `NonExhaustiveError` when cases are missing. */
26
+ 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
+
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
+ /**
36
+ * A pattern that matches a value of type `T`. One of:
37
+ *
38
+ * - **Literal** — a primitive (`"a"`, `42`, `true`) matched by `===`.
39
+ * - **Object pattern** — a `Partial<T>` of shallow key/value pairs that
40
+ * must all match by strict equality. Only available when `T` is an
41
+ * object type.
42
+ * - **Guard** — a function `(input: T) => boolean`. Type predicates
43
+ * (`(x): x is U`) narrow the handler input via {@link Narrow}.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * type Event = { kind: "click"; x: number } | { kind: "key"; code: string };
48
+ *
49
+ * const p1: Pattern<Event> = { kind: "click" }; // object pattern
50
+ * const p2: Pattern<Event> = (e) => e.kind === "key"; // guard
51
+ * ```
52
+ */
53
+ type Pattern<T> = T | ObjectPattern<T> | PatternGuard<T>;
54
+ type ObjectPattern<T> = T extends object ? Partial<T> : never;
55
+ type PatternGuard<T> = (input: T) => boolean;
56
+ type Case<T, R> = {
57
+ readonly test: (input: T) => boolean;
58
+ readonly run: (input: T) => R;
59
+ };
60
+ type NextResult<Locked extends boolean, R, R2> = Locked extends true ? R : R | R2;
61
+ type HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2;
62
+ /**
63
+ * Fluent builder for {@link match}. Tracks the handled cases at the type
64
+ * level so `.exhaustive()` only compiles when every union member is covered.
65
+ *
66
+ * The `Locked` phantom flag (set via `.returnType<R>()`) constrains every
67
+ * subsequent handler to return `R`, useful when branch return-type
68
+ * inference would widen to a union narrower than the target slot
69
+ * (e.g. `ReactNode`, an API DTO).
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * const describe = match<Event>()
74
+ * .with({ kind: "click" }, (e) => `click @ ${e.x},${e.y}`)
75
+ * .with({ kind: "key" }, (e) => `key ${e.code}`)
76
+ * .exhaustive();
77
+ * ```
78
+ */
79
+ declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handled extends readonly unknown[] = [], Locked extends boolean = false> {
80
+ private readonly cases;
81
+ private readonly input?;
82
+ readonly _handled: Handled;
83
+ constructor(cases?: readonly Case<T, unknown>[], input?: T | undefined, _handled?: Handled);
84
+ 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
+ /** One handler for several patterns (OR). Handler input is the union of narrowed members. */
86
+ 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>;
87
+ /** `withOneOf` for exactly two patterns. */
88
+ 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>;
89
+ /**
90
+ * Lock the result type. All subsequent `.with()` handlers must return `R2`.
91
+ * Useful when branch return-type inference widens to a union narrower than
92
+ * the slot the match feeds into (e.g. `ReactNode`).
93
+ */
94
+ returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true>;
95
+ /** Run on the value passed to {@link match}, or on `input` when curried. */
96
+ run(input?: T): R;
97
+ exhaustive(): ExhaustiveResult<T, Handled, R, HasInput>;
98
+ otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R;
99
+ }
100
+ /**
101
+ * Result-locked variant of {@link MatchBuilder}. Constructed via
102
+ * `match(...).returnType<R>()`. Now a thin type alias over `MatchBuilder`
103
+ * with the `Locked` phantom flag set — kept for backwards compatibility.
104
+ */
105
+ type LockedMatchBuilder<T, R, HasInput extends boolean = false, Handled extends readonly unknown[] = []> = MatchBuilder<T, R, HasInput, Handled, true>;
106
+ /**
107
+ * Start a pattern-matching expression. Two call shapes:
108
+ *
109
+ * - `match(value)` — data-first; subsequent `.exhaustive()` or
110
+ * `.otherwise(fn)` runs immediately against `value`.
111
+ * - `match<T>()` — curried; subsequent `.exhaustive()` returns a function
112
+ * `(value: T) => R` for use in `pipe(...)` or as a reusable matcher.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * // Data-first
117
+ * const out = match({ kind: "click", x: 1, y: 2 } as Event)
118
+ * .with({ kind: "click" }, (e) => e.x + e.y)
119
+ * .with({ kind: "key" }, () => -1)
120
+ * .exhaustive();
121
+ *
122
+ * // Curried — build a reusable matcher
123
+ * const describe = match<Event>()
124
+ * .with({ kind: "click" }, (e) => `click @ ${e.x},${e.y}`)
125
+ * .with({ kind: "key" }, (e) => `key ${e.code}`)
126
+ * .exhaustive();
127
+ * ```
128
+ */
129
+ declare function match<T>(input: T): MatchBuilder<T, never, true>;
130
+ declare function match<T = never>(): MatchBuilder<T, never, false>;
131
+
132
+ /**
133
+ * Guard pattern for {@link match}.with.
134
+ *
135
+ * Accepts plain boolean guards (`(x) => x.length > 0`) or TS type-predicate
136
+ * guards (`(x): x is Foo => ...`). For type predicates, the matched handler's
137
+ * input is narrowed to the predicate's target type via {@link Narrow}.
138
+ */
139
+ declare function when<T, U extends T>(guard: (input: T) => input is U): (input: T) => input is U;
140
+ declare function when<T>(guard: (input: T) => boolean): Pattern<T>;
141
+
142
+ 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 };
@@ -0,0 +1,142 @@
1
+ export { matchTag } from './tag.js';
2
+
3
+ /** Use in `default` branches after manual narrowing — compile-time exhaustiveness check. */
4
+ declare const assertNever: (value: never, message?: string) => never;
5
+
6
+ type GuardTarget$1<F> = F extends (input: any) => input is infer U ? U : never;
7
+ /**
8
+ * Union members of `T` ruled out by pattern `P`.
9
+ * Boolean guards (non-predicates) do not advance exhaustiveness — use `when(isX)` or `.otherwise()`.
10
+ */
11
+ type ExhaustMatched<T, P> = P extends (input: T) => boolean ? [GuardTarget$1<P>] extends [never] ? never : Extract<T, GuardTarget$1<P>> : Extract<T, P>;
12
+ /** Union of cases already handled by prior branches. */
13
+ type HandledUnion<Handled extends readonly unknown[]> = Handled[number];
14
+ /** Input cases not yet covered by `.with` / `.withOneOf` / `.withEither`. */
15
+ type RemainingCases<T, Handled extends readonly unknown[]> = Exclude<T, HandledUnion<Handled>>;
16
+ type IsExhaustive<T, Handled extends readonly unknown[]> = [
17
+ RemainingCases<T, Handled>
18
+ ] extends [never] ? true : false;
19
+ type ExhaustiveOutput<R, HasInput extends boolean, T> = HasInput extends true ? R : (input: T) => R;
20
+ /** Returned by `.exhaustive()` when union cases are still missing (compile-time error). */
21
+ type NonExhaustiveError<Remaining> = {
22
+ readonly __nonExhaustive: "Add .with() branches for remaining cases";
23
+ readonly remaining: Remaining;
24
+ };
25
+ /** `.exhaustive()` return type — `NonExhaustiveError` when cases are missing. */
26
+ 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
+
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
+ /**
36
+ * A pattern that matches a value of type `T`. One of:
37
+ *
38
+ * - **Literal** — a primitive (`"a"`, `42`, `true`) matched by `===`.
39
+ * - **Object pattern** — a `Partial<T>` of shallow key/value pairs that
40
+ * must all match by strict equality. Only available when `T` is an
41
+ * object type.
42
+ * - **Guard** — a function `(input: T) => boolean`. Type predicates
43
+ * (`(x): x is U`) narrow the handler input via {@link Narrow}.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * type Event = { kind: "click"; x: number } | { kind: "key"; code: string };
48
+ *
49
+ * const p1: Pattern<Event> = { kind: "click" }; // object pattern
50
+ * const p2: Pattern<Event> = (e) => e.kind === "key"; // guard
51
+ * ```
52
+ */
53
+ type Pattern<T> = T | ObjectPattern<T> | PatternGuard<T>;
54
+ type ObjectPattern<T> = T extends object ? Partial<T> : never;
55
+ type PatternGuard<T> = (input: T) => boolean;
56
+ type Case<T, R> = {
57
+ readonly test: (input: T) => boolean;
58
+ readonly run: (input: T) => R;
59
+ };
60
+ type NextResult<Locked extends boolean, R, R2> = Locked extends true ? R : R | R2;
61
+ type HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2;
62
+ /**
63
+ * Fluent builder for {@link match}. Tracks the handled cases at the type
64
+ * level so `.exhaustive()` only compiles when every union member is covered.
65
+ *
66
+ * The `Locked` phantom flag (set via `.returnType<R>()`) constrains every
67
+ * subsequent handler to return `R`, useful when branch return-type
68
+ * inference would widen to a union narrower than the target slot
69
+ * (e.g. `ReactNode`, an API DTO).
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * const describe = match<Event>()
74
+ * .with({ kind: "click" }, (e) => `click @ ${e.x},${e.y}`)
75
+ * .with({ kind: "key" }, (e) => `key ${e.code}`)
76
+ * .exhaustive();
77
+ * ```
78
+ */
79
+ declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handled extends readonly unknown[] = [], Locked extends boolean = false> {
80
+ private readonly cases;
81
+ private readonly input?;
82
+ readonly _handled: Handled;
83
+ constructor(cases?: readonly Case<T, unknown>[], input?: T | undefined, _handled?: Handled);
84
+ 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
+ /** One handler for several patterns (OR). Handler input is the union of narrowed members. */
86
+ 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>;
87
+ /** `withOneOf` for exactly two patterns. */
88
+ 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>;
89
+ /**
90
+ * Lock the result type. All subsequent `.with()` handlers must return `R2`.
91
+ * Useful when branch return-type inference widens to a union narrower than
92
+ * the slot the match feeds into (e.g. `ReactNode`).
93
+ */
94
+ returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true>;
95
+ /** Run on the value passed to {@link match}, or on `input` when curried. */
96
+ run(input?: T): R;
97
+ exhaustive(): ExhaustiveResult<T, Handled, R, HasInput>;
98
+ otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R;
99
+ }
100
+ /**
101
+ * Result-locked variant of {@link MatchBuilder}. Constructed via
102
+ * `match(...).returnType<R>()`. Now a thin type alias over `MatchBuilder`
103
+ * with the `Locked` phantom flag set — kept for backwards compatibility.
104
+ */
105
+ type LockedMatchBuilder<T, R, HasInput extends boolean = false, Handled extends readonly unknown[] = []> = MatchBuilder<T, R, HasInput, Handled, true>;
106
+ /**
107
+ * Start a pattern-matching expression. Two call shapes:
108
+ *
109
+ * - `match(value)` — data-first; subsequent `.exhaustive()` or
110
+ * `.otherwise(fn)` runs immediately against `value`.
111
+ * - `match<T>()` — curried; subsequent `.exhaustive()` returns a function
112
+ * `(value: T) => R` for use in `pipe(...)` or as a reusable matcher.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * // Data-first
117
+ * const out = match({ kind: "click", x: 1, y: 2 } as Event)
118
+ * .with({ kind: "click" }, (e) => e.x + e.y)
119
+ * .with({ kind: "key" }, () => -1)
120
+ * .exhaustive();
121
+ *
122
+ * // Curried — build a reusable matcher
123
+ * const describe = match<Event>()
124
+ * .with({ kind: "click" }, (e) => `click @ ${e.x},${e.y}`)
125
+ * .with({ kind: "key" }, (e) => `key ${e.code}`)
126
+ * .exhaustive();
127
+ * ```
128
+ */
129
+ declare function match<T>(input: T): MatchBuilder<T, never, true>;
130
+ declare function match<T = never>(): MatchBuilder<T, never, false>;
131
+
132
+ /**
133
+ * Guard pattern for {@link match}.with.
134
+ *
135
+ * Accepts plain boolean guards (`(x) => x.length > 0`) or TS type-predicate
136
+ * guards (`(x): x is Foo => ...`). For type predicates, the matched handler's
137
+ * input is narrowed to the predicate's target type via {@link Narrow}.
138
+ */
139
+ declare function when<T, U extends T>(guard: (input: T) => input is U): (input: T) => input is U;
140
+ declare function when<T>(guard: (input: T) => boolean): Pattern<T>;
141
+
142
+ 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 };
package/dist/index.js ADDED
@@ -0,0 +1,125 @@
1
+ // src/assert.ts
2
+ var assertNever = (value, message = "Unreachable") => {
3
+ throw new Error(`${message}: ${String(value)}`);
4
+ };
5
+
6
+ // src/match.ts
7
+ var isGuard = (pattern) => typeof pattern === "function";
8
+ var isPatternObject = (pattern) => typeof pattern === "object" && pattern !== null;
9
+ var matches = (input, pattern) => {
10
+ if (isGuard(pattern)) {
11
+ return pattern(input);
12
+ }
13
+ if (!isPatternObject(pattern)) {
14
+ return input === pattern;
15
+ }
16
+ if (typeof input !== "object" || input === null) {
17
+ return false;
18
+ }
19
+ const record = input;
20
+ const patternRecord = pattern;
21
+ for (const key of Object.keys(patternRecord)) {
22
+ if (record[key] !== patternRecord[key]) {
23
+ return false;
24
+ }
25
+ }
26
+ return true;
27
+ };
28
+ var NO_MATCH = /* @__PURE__ */ Symbol("@onrails/pattern/no-match");
29
+ var runCases = (input, cases) => {
30
+ for (const c of cases) {
31
+ if (c.test(input)) {
32
+ return c.run(input);
33
+ }
34
+ }
35
+ return NO_MATCH;
36
+ };
37
+ var caseForPatterns = (patterns, handler) => ({
38
+ test: (input) => patterns.some((pattern) => matches(input, pattern)),
39
+ run: handler
40
+ });
41
+ var MatchBuilder = class _MatchBuilder {
42
+ constructor(cases = [], input, _handled = []) {
43
+ this.cases = cases;
44
+ this.input = input;
45
+ this._handled = _handled;
46
+ }
47
+ cases;
48
+ input;
49
+ _handled;
50
+ with(pattern, handler) {
51
+ const next = {
52
+ test: (input) => matches(input, pattern),
53
+ run: handler
54
+ };
55
+ return new _MatchBuilder([...this.cases, next], this.input, [
56
+ ...this._handled
57
+ ]);
58
+ }
59
+ /** One handler for several patterns (OR). Handler input is the union of narrowed members. */
60
+ withOneOf(patterns, handler) {
61
+ const next = caseForPatterns(patterns, handler);
62
+ return new _MatchBuilder([...this.cases, next], this.input, [
63
+ ...this._handled
64
+ ]);
65
+ }
66
+ /** `withOneOf` for exactly two patterns. */
67
+ withEither(pattern1, pattern2, handler) {
68
+ return this.withOneOf([pattern1, pattern2], handler);
69
+ }
70
+ /**
71
+ * Lock the result type. All subsequent `.with()` handlers must return `R2`.
72
+ * Useful when branch return-type inference widens to a union narrower than
73
+ * the slot the match feeds into (e.g. `ReactNode`).
74
+ */
75
+ returnType() {
76
+ return new _MatchBuilder(this.cases, this.input, this._handled);
77
+ }
78
+ /** 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) {
82
+ throw new Error("match.run: no input value");
83
+ }
84
+ const out = runCases(value, this.cases);
85
+ if (out === NO_MATCH) {
86
+ throw new Error("Non-exhaustive match: no case matched input");
87
+ }
88
+ return out;
89
+ }
90
+ exhaustive() {
91
+ if (this.input !== void 0) {
92
+ return this.run();
93
+ }
94
+ return ((value) => this.run(value));
95
+ }
96
+ otherwise(handler) {
97
+ const runWithFallback = (value) => {
98
+ const out = runCases(value, this.cases);
99
+ return out === NO_MATCH ? handler(value) : out;
100
+ };
101
+ if (this.input !== void 0) {
102
+ return runWithFallback(this.input);
103
+ }
104
+ return runWithFallback;
105
+ }
106
+ };
107
+ function match(input) {
108
+ return new MatchBuilder([], input);
109
+ }
110
+
111
+ // src/tag.ts
112
+ var matchTag = (value, handlers) => {
113
+ const tag = value._tag;
114
+ const handler = handlers[tag];
115
+ return handler(value);
116
+ };
117
+
118
+ // src/when.ts
119
+ function when(guard) {
120
+ return guard;
121
+ }
122
+
123
+ export { MatchBuilder, assertNever, match, matchTag, when };
124
+ //# sourceMappingURL=index.js.map
125
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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\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"]}
package/dist/tag.cjs ADDED
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ // src/tag.ts
4
+ var matchTag = (value, handlers) => {
5
+ const tag = value._tag;
6
+ const handler = handlers[tag];
7
+ return handler(value);
8
+ };
9
+
10
+ exports.matchTag = matchTag;
11
+ //# sourceMappingURL=tag.cjs.map
12
+ //# sourceMappingURL=tag.cjs.map
@@ -0,0 +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"]}
package/dist/tag.d.cts ADDED
@@ -0,0 +1,14 @@
1
+ type Tagged = {
2
+ readonly _tag: string;
3
+ };
4
+ type TagHandlers<T extends Tagged, R> = {
5
+ [K in T["_tag"]]: (value: Extract<T, {
6
+ _tag: K;
7
+ }>) => R;
8
+ };
9
+ /**
10
+ * Exhaustive match on `_tag` — for `@onrails/result` / `@onrails/maybe` style unions.
11
+ */
12
+ declare const matchTag: <T extends Tagged, R>(value: T, handlers: TagHandlers<T, R>) => R;
13
+
14
+ export { matchTag };
package/dist/tag.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ type Tagged = {
2
+ readonly _tag: string;
3
+ };
4
+ type TagHandlers<T extends Tagged, R> = {
5
+ [K in T["_tag"]]: (value: Extract<T, {
6
+ _tag: K;
7
+ }>) => R;
8
+ };
9
+ /**
10
+ * Exhaustive match on `_tag` — for `@onrails/result` / `@onrails/maybe` style unions.
11
+ */
12
+ declare const matchTag: <T extends Tagged, R>(value: T, handlers: TagHandlers<T, R>) => R;
13
+
14
+ export { matchTag };
package/dist/tag.js ADDED
@@ -0,0 +1,10 @@
1
+ // src/tag.ts
2
+ var matchTag = (value, handlers) => {
3
+ const tag = value._tag;
4
+ const handler = handlers[tag];
5
+ return handler(value);
6
+ };
7
+
8
+ export { matchTag };
9
+ //# sourceMappingURL=tag.js.map
10
+ //# sourceMappingURL=tag.js.map
@@ -0,0 +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"]}
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@onrails/pattern",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight exhaustive matching for owned tagged unions — ts-pattern-shaped DX, tree-shakeable",
5
+ "license": "MIT",
6
+ "author": "Alan R. Soares",
7
+ "homepage": "https://alanrsoares.github.io/onrails/modules/_onrails_pattern.html",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/alanrsoares/onrails.git",
11
+ "directory": "packages/pattern"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/alanrsoares/onrails/issues"
15
+ },
16
+ "keywords": [
17
+ "pattern-matching",
18
+ "exhaustive",
19
+ "tagged-union",
20
+ "discriminated-union",
21
+ "typescript",
22
+ "bun",
23
+ "functional"
24
+ ],
25
+ "engines": {
26
+ "node": ">=18",
27
+ "bun": ">=1.0"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "provenance": true
32
+ },
33
+ "type": "module",
34
+ "sideEffects": false,
35
+ "main": "./dist/index.cjs",
36
+ "module": "./dist/index.js",
37
+ "types": "./dist/index.d.ts",
38
+ "files": [
39
+ "dist",
40
+ "DESIGN.md",
41
+ "README.md",
42
+ "LICENSE"
43
+ ],
44
+ "exports": {
45
+ ".": {
46
+ "types": "./dist/index.d.ts",
47
+ "import": "./dist/index.js",
48
+ "require": "./dist/index.cjs"
49
+ },
50
+ "./tag": {
51
+ "types": "./dist/tag.d.ts",
52
+ "import": "./dist/tag.js",
53
+ "require": "./dist/tag.cjs"
54
+ }
55
+ },
56
+ "scripts": {
57
+ "typecheck": "tsc --noEmit",
58
+ "test": "bun test",
59
+ "check": "bun typecheck && bun test",
60
+ "build": "tsup",
61
+ "clean": "rm -rf dist",
62
+ "prepublishOnly": "bun run build"
63
+ },
64
+ "devDependencies": {
65
+ "@onrails/result": "0.0.0",
66
+ "ts-expect": "^1.3.0",
67
+ "tsup": "^8.5.1"
68
+ }
69
+ }