npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tiny-pattern-ts

v0.9.0

Published

Exhaustive, type-safe pattern matching for TypeScript

Readme

tiny-pattern-ts

Exhaustive, type-safe pattern matching for TypeScript.

Synopsis

import { getTaggedUnionMatcher } from "tiny-pattern-ts";

// 1. We have a union type
type Contact =
    | { kind: "email"; address: string }
    | { kind: "phone"; number: string }
    | { kind: "messenger"; username: string };

// 2. Create a matcher providing the discriminant property
const matchContact = getTaggedUnionMatcher<Contact>()("kind");

// 3. Define handlers for each branch of the union
const formatContact = matchContact({
    email: (e) => `MAIL: ${e.address}`,
    phone: (p) => `PHONE: ${p.number}`,
    messenger: (m) => `MESSENGER: @${m.username}`,
});

// 4. Call the matcher with a value
const mailOutput = formatContact({ kind: "email", address: "[email protected]" });
assert.equal(mailOutput, "MAIL: [email protected]");

const phoneOutput = formatContact({ kind: "phone", number: "+1 555 0100" });
assert.equal(phoneOutput, "PHONE: +1 555 0100");

Description

tiny-pattern-ts is a pattern-matching library for TypeScript.

The main goal of tiny-pattern-ts is to make pattern matching type-safe with a lean syntax. This is accomplished by being exhaustive and passing typed parameters per branch to the handlers — supported by an outstanding autocomplete and a tiny footprint. The matchers are data last and pipe-friendly: build the handler map once, then apply the resulting matcher to values (match(value), or pipe(value, match)).

See development/library.md for the design decisions and Caveats for the limits.

Installation

npm install tiny-pattern-ts

Requirements

  • Node.js >= 26 (engines field; pinned via .node-version).
  • TypeScript >= 5.9 to consume the published declarations. The floor is set by the type-fest types the declarations use and is checked in CI against a consumer fixture; see compat/ and development/ci.md § TypeScript compatibility. The emitted .d.ts keep their relative .ts specifiers, which resolve under node16 / nodenext / bundler. The package exposes only an exports map (no main / top-level types), so the legacy node10 resolver does not apply.
  • The package is ESM-only (no CommonJS shim).

Examples

A few real-world recipes. Each binds the handler-map function once and reuses it, so the matcher is allocated a single time.

Dispatch on a primitive union

A result code is itself a finite union, so getPrimitiveUnionMatcher keys a handler on each member:

import { getPrimitiveUnionMatcher } from "tiny-pattern-ts";

type ResultCode = "ok" | "created" | "no-content";

const toStatus = getPrimitiveUnionMatcher<ResultCode>()({
    ok: () => 200,
    created: () => 201,
    "no-content": () => 204,
});

assert.equal(toStatus("ok"), 200);
assert.equal(toStatus("created"), 201);
assert.equal(toStatus("no-content"), 204);

Leave cases to a fallback

Pass a fallback as the second argument to handle only part of the universe; it receives the members the map leaves uncovered — here the parameter is "deprecated" | "gateway-timeout":

import { getPrimitiveUnionMatcher } from "tiny-pattern-ts";

type Status = "active" | "beta" | "deprecated" | "gateway-timeout";

const rollout = getPrimitiveUnionMatcher<Status>()(
    { active: () => "enabled", beta: () => "enabled" },
    (status) => `blocked (${status})`,
);

assert.equal(rollout("active"), "enabled");
assert.equal(rollout("deprecated"), "blocked (deprecated)");

Dispatch on a property union

The value does not have to be the union itself. When a single property carries a finite union, the tagged-union matcher keys on it and narrows the whole record to the selected value:

import { getTaggedUnionMatcher } from "tiny-pattern-ts";

interface Invoice {
    readonly currency: "eur" | "usd" | "jpy";
    readonly amount: number;
}

const matchCurrency = getTaggedUnionMatcher<Invoice>()("currency");

const symbolOf = matchCurrency({
    eur: (i) => `€${i.amount.toFixed(2)}`,
    usd: (i) => `$${i.amount.toFixed(2)}`,
    jpy: (i) => `¥${i.amount.toFixed(0)}`,
});

assert.equal(symbolOf({ currency: "usd", amount: 12.5 }), "$12.50");
assert.equal(symbolOf({ currency: "jpy", amount: 900 }), "¥900");

Widen the return type

When the handlers return different types, reach for the widening W variant: the matcher's return is their union rather than one common type — here string | string[] | undefined:

import { getPrimitiveUnionMatcherW } from "tiny-pattern-ts";

type Field = "name" | "tags" | "note";

const parse = getPrimitiveUnionMatcherW<Field>()({
    name: () => "Ada",
    tags: () => ["admin", "beta"],
    note: () => undefined,
});

assert.equal(parse("name"), "Ada");
assert.deepEqual(parse("tags"), ["admin", "beta"]);
assert.equal(parse("note"), undefined);

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 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",
});

const answer = reply("yes");
assert.equal(answer, "agreed");

Add a fallback as the second argument to leave members unhandled; the fallback receives the remainder:

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<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,
});

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

Alternatives

ts-pattern

  • is the full structural matcher — nested and partial patterns, guards, unions, captures — exhausting at .exhaustive()
  • has a fluent .with(…) chain that is heavy on syntax; tiny-pattern-ts is one handler map
  • is about 2 kB minified and gzipped; tiny-pattern-ts is 0.2 kB
  • reach for it when you need a feature tiny-pattern-ts does not cover

Effect's Match

  • is the same piped matcher (Match.type / Match.when / Match.exhaustive), but only as part of the effect ecosystem
  • tiny-pattern-ts is standalone: no runtime dependency to buy into

match-iz

  • expresses patterns in the TC39 proposal's style, deciding each case at runtime
  • is written in JavaScript with hand-maintained declarations, so its types do not prove the cases exhaustive
  • tiny-pattern-ts does: exhaustiveness is a compile-time guarantee, not an otherwise fallback

plain switch (baseline)

  • is the zero-dependency baseline — pair it with eslint-plugin-strict-pattern-matching for exhaustiveness
  • is a statement, not an expression, so it cannot produce a value directly
  • leaves the never guard to you; tiny-pattern-ts is an expression and does not need one

The TC39 pattern-matching proposal is still stage 1, so userland libraries remain the only option today.

License

MIT © 2026 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.