# 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 and the builder types they return. Each factory comes in a _strict_ variant (one common return type `R`) and a _widening_ variant (the union of every handler's return type): | Factory | Builder type | Return of the matcher | | -------------------------------- | ------------------------------- | -------------------------------- | | `getPrimitiveUnionMatcher()` | `PrimitiveUnionMatcher` | one common `R` | | `getPrimitiveUnionMatcherW()` | `PrimitiveUnionMatcherW` | the union of the handler returns | | `getTaggedUnionMatcher()` | `TaggedUnionMatcherFactory` | one common `R` | | `getTaggedUnionMatcherW()` | `TaggedUnionMatcherWFactory` | the union of the handler returns | ### 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 { getPrimitiveUnionMatcher } from "tiny-pattern-ts"; const reply = getPrimitiveUnionMatcher<"yes" | "no">()({ yes: () => "agreed", no: () => "declined", }); reply("yes"); // "agreed" ``` Add a fallback as the second argument to leave members unhandled; the fallback receives the remainder: ```ts const label = getPrimitiveUnionMatcher<"yes" | "no" | "maybe">()( { yes: () => "agreed", no: () => "declined" }, (other) => `not sure: ${other}`, // other: "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 { getTaggedUnionMatcher } from "tiny-pattern-ts"; type Shape = { kind: "circle"; radius: number } | { kind: "square"; side: number }; const area = getTaggedUnionMatcher()("kind")({ circle: (s) => Math.PI * s.radius ** 2, square: (s) => s.side ** 2, }); area({ kind: "circle", radius: 2 }); ``` `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 © 2025 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).