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

@ossedb/patmat

v1.0.0

Published

Typed pattern matching and algebraic data types for JavaScript and TypeScript

Readme

patmat

Typed pattern matching and algebraic data types for JavaScript and TypeScript — in Node and the browser, with zero dependencies.

import { match, P } from '@ossedb/patmat';

const fact = (n: number): number =>
  match(n)
    .with(0, () => 1)
    .with(P.bind('n', P.number), ({ n }) => n * fact(n - 1))
    .exhaustive();

fact(5); // 120

Patterns are plain values: typos are compile-time errors, captures are typed, and TypeScript checks ADT matches for exhaustiveness.

Install

npm install @ossedb/patmat

The package is ESM-only and environment-agnostic (no Node APIs), so it works anywhere modern JavaScript runs. Node ≥ 20.19 can also require() it from CommonJS. In the browser, use a bundler or a CDN:

<script type="module">
  import { match, P } from 'https://esm.sh/@ossedb/patmat';
</script>

Matching values

match(value) starts a chain of clauses. The first pattern that matches wins; later clauses are not evaluated. End the chain with .otherwise(handler) or .exhaustive() (throws NonExhaustiveError if nothing matched).

const hello = (value: unknown): string =>
  match(value)
    .with(0, () => 'Ooops..')
    .otherwise(() => 'Hello World!');

hello(0);   // 'Ooops..'
hello(347); // 'Hello World!'

Arrays

Array patterns match tuples element-wise with exact length; a trailing P.rest relaxes the length and can capture the remainder. The classic head/tail recursion:

const sum = (xs: number[]): number =>
  match(xs)
    .with([], () => 0)
    .with([P.bind('x'), P.rest('xs')], ({ x, xs }) => (x as number) + sum(xs as number[]))
    .exhaustive();

sum([1, 4, 3]); // 8

Objects

Plain-object patterns match structurally: every key in the pattern must match on the value, extra keys are allowed.

match(response)
  .with({ status: 'ok', data: P.bind('data') }, ({ data }) => data)
  .with({ status: 'error' }, () => null)
  .otherwise(() => null);

Guards

A clause can take a guard between the pattern and the handler. A failed guard falls through to the next clause.

const describeNumber = (n: number): string =>
  match(n)
    .with(
      P.bind('n', P.number),
      ({ n }) => n % 2 === 0,
      ({ n }) => `${n} is even`,
    )
    .otherwise((n) => `${n} is odd`);

describeNumber(4); // '4 is even'
describeNumber(7); // '7 is odd'

Pattern reference

| Pattern | Matches | | --- | --- | | 0, 'a', true, null, undefined, 1n | that literal, compared with Object.is | | [p1, p2] | arrays of exactly that length, element-wise | | [p1, P.rest('xs')] | arrays with at least the fixed elements; remainder captured | | { key: p } | objects where each pattern key matches (extra keys allowed) | | P._ | anything (wildcard) | | P.number, P.string, P.boolean | values of that primitive type | | P.nullish | null or undefined | | P.bind(name) | anything, capturing the value under name | | P.bind(name, pattern) | the inner pattern, capturing the value with the inner pattern's type | | P.array() / P.array(element) | any array / arrays whose every element matches | | P.range(lo, hi) | numbers in the inclusive range | | P.when(predicate) | values for which the predicate returns true | | P.union(...patterns) | values matching any branch (or-pattern) | | P.instanceOf(Class) | instances of Class |

const grade = (score: number): string =>
  match(score)
    .with(P.range(90, 100), () => 'A')
    .with(P.range(80, 89), () => 'B')
    .otherwise(() => 'C or below');

const kind = (value: unknown): string =>
  match(value)
    .with(P.union(P.string, P.number), () => 'text or number')
    .with(P.instanceOf(Date), () => 'a date')
    .otherwise(() => 'something else');

isMatch(pattern, value) tests a pattern standalone, without handlers:

isMatch([P.number, P.rest()], [1, 2, 3]);            // true
isMatch({ status: 'ok' }, { status: 'ok', data: [] }); // true

Algebraic data types

adt(name, spec) defines a variant type. Constructors come back on the returned object — nothing touches the global scope (unlike the 2014 API). Each spec entry is either a function from constructor arguments to the case's fields, or null for a nullary case, which becomes a frozen singleton value.

import { adt, type VariantOf } from '@ossedb/patmat';

const Shape = adt('Shape', {
  Circle: (radius: number) => ({ radius }),
  Rect: (w: number, h: number) => ({ w, h }),
});
type Shape = VariantOf<typeof Shape>;

const area = (s: Shape): number =>
  Shape.match(s, {
    Circle: ({ radius }) => Math.PI * radius ** 2,
    Rect: ({ w, h }) => w * h,
  });

area(Shape.Rect(2, 5)); // 10

Instances are plain frozen records with a tag discriminant and flat fields ({ tag: 'Rect', w: 2, h: 5 }), so they serialize cleanly with JSON.stringify and match structurally in the general match:

match<Shape>(Shape.Circle(3))
  .with({ tag: 'Circle', radius: P.bind('r', P.number) }, ({ r }) => `circle r=${r}`)
  .otherwise(() => 'not a circle');

Guards come for free: Shape.is(v) narrows to the union, Shape.Circle.is(v) to the case, and Shape.Circle.tag is the tag literal.

Exhaustiveness

Shape.match is statically exhaustive: omitting a case is a TypeScript compile error. For a partial match, pass a _ fallback:

Shape.match(s, {
  Circle: ({ radius }) => `circle of radius ${radius}`,
  _: () => 'some other shape',
});

The general match(...).exhaustive() is checked at runtime (it throws NonExhaustiveError, which carries the offending value). This split is deliberate: static exhaustiveness is sound and cheap for discriminated unions, while doing it for arbitrary patterns would require heavyweight type machinery.

Recursive ADTs

Recursive types declare their variant union manually (the spec's fields need to refer to it — this is the documented limit of VariantOf inference):

import { adt, type Variant } from '@ossedb/patmat';

type List = Variant<'Empty'> | Variant<'Cons', { head: number; tail: List }>;
const List = adt('List', {
  Empty: null,
  Cons: (head: number, tail: List) => ({ head, tail }),
});

const sum = (l: List): number =>
  List.match(l, {
    Empty: () => 0,
    Cons: ({ head, tail }) => head + sum(tail),
  });

sum(List.Cons(1, List.Cons(2, List.Cons(3, List.Empty)))); // 6

Semantics, precisely

  • First match wins. Clauses are tried in order; there is no redundancy analysis.
  • Primitives use Object.is. So NaN matches NaN, and -0 does not match 0.
  • Object patterns are subsets. Extra keys on the value never fail a match.
  • Array patterns are exact-length unless they end in P.rest(...).
  • Other reference types are rejected as patterns. A Date or Map used as a pattern throws a TypeError; match them with P.instanceOf or P.when. (They are fine as values.)
  • Capture types come from the pattern, not the value. P.bind('x') captures unknown; write P.bind('x', P.number) to capture number. Captures are not narrowed from the matched value's type, and clause patterns don't narrow the input type — this keeps the type-level machinery small and predictable.
  • Duplicate capture names: the last write wins.

API documentation

Generated API docs (TypeDoc) live at the project's GitHub Pages site, or build them locally:

npm run docs

Development

npm install
npm run lint        # Biome
npm run typecheck   # tsc
npm test            # Vitest
npm run test:types  # type-level tests
npm run build       # tsdown -> dist/

The published library supports Node >=20.19, but npm run build needs Node >=22.18 (tsdown's floor).

Every example in this README runs as a test in test/readme.test.ts.

License

MIT