tmu c878e60ff2 📝 Bind the handler-map function in every example
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.
2026-09-23 22:33:43 +00:00
2026-09-23 21:12:24 +00:00
2026-09-08 20:47:22 +02:00
2026-09-14 12:37:58 +00:00
2026-09-06 00:13:12 +02:00
2026-09-22 09:43:08 +00:00
2026-09-23 22:19:02 +00:00
2026-02-02 13:38:42 +01:00
2026-09-23 21:12:24 +00:00
2026-09-23 14:55:27 +00:00
2025-04-29 13:43:07 +02:00
2026-09-23 21:24:42 +00:00
2026-09-23 21:24:42 +00:00

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 (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<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, 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.

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.

S
Description
No description provided
Readme MIT
1.5 MiB
0 Stars 1 Watchers 0 Forks
0.9.0
Latest
2026-09-29 23:57:54 +02:00
Languages
TypeScript 88.4%
Shell 9.9%
Dockerfile 1.7%