Each README and JSDoc example now assigns the function that takes the handler object (the factory result, or the tagged key step) to a match… variable and reuses it, so the builder is created once instead of per call.
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
for the design decisions and Caveats for the limits.
Requirements
- Node.js >= 26 (
enginesfield; pinned via.node-version). - TypeScript >= 5.0 to consume the published declarations. The emitted
.d.tsuseconsttype parameters (TS 5.0) and keep their relative.tsspecifiers; both resolve on TS >= 5.0 innode10/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<T>() |
the value is the union and all handlers return the same type | one common R |
getPrimitiveUnionMatcherW<T>() |
the value is the union and handlers return different types | union of the handlers |
getTaggedUnionMatcher<T>() |
the value is a discriminated object and all handlers return the same type | one common R |
getTaggedUnionMatcherW<T>() |
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<T>() 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.
import { getPrimitiveUnionMatcher } from "tiny-pattern-ts";
const matchAnswer = getPrimitiveUnionMatcher<"yes" | "no">();
const reply = matchAnswer({
yes: () => "agreed",
no: () => "declined",
});
reply("yes"); // "agreed"
Add a fallback as the second argument to leave members unhandled; the fallback receives the remainder:
const matchLabel = getPrimitiveUnionMatcher<"yes" | "no" | "maybe">();
const label = matchLabel(
{ 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<T>() 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.
import { getTaggedUnionMatcher } from "tiny-pattern-ts";
type Shape =
{ kind: "circle"; radius: number } | { kind: "square"; side: number };
const matchShape = getTaggedUnionMatcher<Shape>()("kind");
const area = matchShape({
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 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.
Caveats
- Only finite universes are supported. The factory must be given a finite
union of literals;
string,numberand template literals are rejected. This is what lets the exhaustive overload be proven, so the runtimedispatchthrow 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). symbolandbigintare not supported. Asymbolbrand is a compile-time phantom with nothing to match at runtime, and abigintis not a valid property key; neither satisfies the matcher's universe constraint.NaNand-0cannot be matched specifically. They have no literal type, so both stay part ofnumber.
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.
License
MIT © 2025 tmu. See LICENSE.
Contributing
Contributions are documented in CONTRIBUTING.md; the reasons behind the project's decisions, rejected alternatives, and known issues live in development/. AI coding agents start at AGENTS.md.