# tiny-pattern-ts Pattern matching for TypeScript/ESM environments (F#-style, not regex). ## Description `tiny-pattern-ts` brings F#-style pattern matching to TypeScript. Patterns are ordinary objects whose `matches` method is a TypeScript type guard, so narrowing composes the way any other guard does. It is deliberately not a regex engine and not a macro: there is no transpiler and no DSL to learn, and the type-level contract is the feature — see [development/library.md](./development/library.md) for the design decisions and [Caveats](#caveats) for the limits. ## Requirements - **Node.js >= 26** (`engines` field; pinned via `.node-version`). - **TypeScript >= 5.0** to consume the published declarations. The emitted `.d.ts` use `const` type parameters (TS 5.0) and keep their relative `.ts` specifiers; both resolve on TS >= 5.0 in `node10` / `node16` / `nodenext` / `bundler`. - The package is **ESM-only** (no CommonJS shim). ## API The package exports four factories. Two axes pick one: - **Universe** — a _primitive-union_ matcher matches a value that is itself a finite union (`"yes" | "no"`); a _tagged-union_ matcher matches an object discriminated by a property (`{ kind: … }`). - **Return** — the _strict_ variant gives every handler one common return type `R`; the _widening_ variant (`W`) widens the return value to the union of the handler returns. | Factory | Use when | Return | | -------------------------------- | ------------------------------------------------------------------------- | --------------------- | | `getPrimitiveUnionMatcher()` | the value is the union and all handlers return the same type | one common `R` | | `getPrimitiveUnionMatcherW()` | the value is the union and handlers return different types | union of the handlers | | `getTaggedUnionMatcher()` | the value is a discriminated object and all handlers return the same type | one common `R` | | `getTaggedUnionMatcherW()` | the value is a discriminated object and handlers return different types | union of the handlers | Bind the function that takes the handler map to a `match…` variable once and reuse it; the examples below do this, so the builder is allocated once. ### Primitive-union matchers `getPrimitiveUnionMatcher()` takes the finite universe `T` and returns a builder. Calling the builder with a handler map keyed by `T`'s members returns a matcher: a function from `T` to the common return type. ```ts import { strict as assert } from "node:assert"; import { getPrimitiveUnionMatcher } from "tiny-pattern-ts"; const matchAnswer = getPrimitiveUnionMatcher<"yes" | "no">(); const reply = matchAnswer({ yes: () => "agreed", no: () => "declined", }); const answer = reply("yes"); assert.equal(answer, "agreed"); ``` Add a fallback as the second argument to leave members unhandled; the fallback receives the remainder: ```ts import { strict as assert } from "node:assert"; import { getPrimitiveUnionMatcher } from "tiny-pattern-ts"; const matchLabel = getPrimitiveUnionMatcher<"yes" | "no" | "maybe">(); const label = matchLabel( { yes: () => "agreed", no: () => "declined" }, (other) => `not sure: ${other}`, // other: "maybe" ); const answer = label("yes"); assert.equal(answer, "agreed"); const fallback = label("maybe"); assert.equal(fallback, "not sure: maybe"); ``` `getPrimitiveUnionMatcherW` is the same builder, but the matcher's return type is the union of the handler return types rather than one common `R`. ### Tagged-union matchers `getTaggedUnionMatcher()` takes a discriminated union `T`. The returned function takes the discriminant property's name and returns the handler-map builder, keyed by that property's tags. ```ts import { strict as assert } from "node:assert"; import { getTaggedUnionMatcher } from "tiny-pattern-ts"; type Shape = { kind: "circle"; radius: number } | { kind: "square"; side: number }; const matchShape = getTaggedUnionMatcher()("kind"); const area = matchShape({ circle: (s) => Math.PI * s.radius ** 2, square: (s) => s.side ** 2, }); const circleArea = area({ kind: "circle", radius: 2 }); assert.equal(circleArea, Math.PI * 4); const squareArea = area({ kind: "square", side: 3 }); assert.equal(squareArea, 9); ``` `getTaggedUnionMatcherW` is the widening counterpart, exactly as in the primitive-union pair. The discriminant key is restricted to properties whose values are tags; see [Caveats](#caveats) for the supported tags and the `boolean` / `null` / `undefined` key projection. ### The universe The primitive universe `T` must be a finite union of literals with no value/stringification collision. A broad member (`string`, `number`, a template literal) or a colliding pair (`true | "true"`, `1 | "1"`) is rejected at the factory. The reasons and the rejected alternatives are in [development/library.md](./development/library.md). ## Caveats - **Only finite universes are supported.** The factory must be given a finite union of literals; `string`, `number` and template literals are rejected. This is what lets the exhaustive overload be proven, so the runtime `dispatch` throw stays unreachable through the typed API. - **A value and its stringification must not both be present.** Object keys stringify, so a universe containing both a member and the string it stringifies to — `1 | "1"`, `true | "true"`, `null | "null"` — is rejected at the factory. Either form alone is fine, and one value's string form may coexist with a _different_ value's bare form (`"true" | false`). - **`symbol` and `bigint` are not supported.** A `symbol` brand is a compile-time phantom with nothing to match at runtime, and a `bigint` is not a valid property key; neither satisfies the matcher's universe constraint. - **`NaN` and `-0` cannot be matched specifically.** They have no literal type, so both stay part of `number`. ### Why open universes are rejected An open universe — one carrying a broad member, as in `type Units = "s" | "ms" | "min" | (string & {})` — is not a dispatch concern. If the values arrive from outside the program, parse them at the boundary down to a finite union and match the narrowed result; the openness never reaches the matcher. If the domain is genuinely extensible, the right shape is a runtime `Map` of handlers, where "no handler" is a lookup, not a pattern. Either way an open matcher would abandon the one guarantee this library exists to give — provable exhaustiveness — to automate what a `switch` and a default arm already cover. The type-level cost of supporting open universes is recorded in [development/library.md](./development/library.md#supported-universes). ## License MIT © 2026 tmu. See [LICENSE](./LICENSE). ## Contributing Contributions are documented in [CONTRIBUTING.md](./CONTRIBUTING.md); the reasons behind the project's decisions, rejected alternatives, and known issues live in [development/](./development/README.md). AI coding agents start at [AGENTS.md](./AGENTS.md).