Skip to content

match โ€‹

bundle

Zero-runtime exhaustive pattern matching, inspired by ts-pattern and pattycake.

This library is a lightweight alternative to ts-pattern, with an optional Babel plugin that compiles match expressions to fast, plain JavaScript.

Features โ€‹

  • Very small size
  • Up to 60x faster than ts-pattern
  • 600+ tests
  • Type-safe exhaustiveness checks
  • Match against literals, primitives, and shallow objects
  • Eager (case(pattern, result)) and lazy (case(pattern, () => result)) matching
  • Fallback methods: or, orElse, orThrow

Examples โ€‹

ts
import { match } from "@monstermann/match"

// Match primitives:
match(value)
    .case(1, 2)
    .case(2, 3)
    .orElse(v => v + 1)

// Match object shapes:
match({ value: foo })
    .shape({ value: 1 }, 2)
    .shape({ value: 2 }, 3)
    .or(0)

// Match with conditions:
match({ value: foo })
    .cond(v => v.value > 0, "positive")
    .cond(v => v.value < 0, "negative")
    .or("zero")

Babel plugin โ€‹

The Babel plugin can optimize your code to be as fast as hand-written if/else statements.

ts
import { match } from "@monstermann/match"

match(value)
    .case(1, 2)
    .case(2, 3)
    .orElse(v => v + 1)
ts
  value === 1 ? 2 
: value === 2 ? 3 
: value + 1
ts
import { match } from "@monstermann/match"

match({ value: foo })
    .shape({ value: 1 }, 2)
    .shape({ value: 2 }, 3)
    .or(0)
ts
  foo === 1 ? 2
: foo === 2 ? 3
: 0
ts
import { match } from "@monstermann/match"

match({ value: foo })
    .cond(v => v.value > 0, "positive")
    .cond(v => v.value < 0, "negative")
    .or("zero")
ts
  foo > 0 ? "positive" 
: foo < 0 ? "negative" 
: "zero"
ts
import { match } from "@monstermann/match"

match(expensive())
    .shape({ value: undefined }, "undefined")
    .cond((v) => isString(v.value), "string")
    .cond((v) => isNumber(v.value), "number")
    .or("unknown")
ts
((_v) =>
    _v && typeof _v === "object" && _v.value === undefined ? "undefined"
    : isString(_v.value) ? "string"
    : isNumber(_v.value) ? "number"
    : "unknown"
)(expensive())

Benchmarks โ€‹

  • Runtime: Node v24.0.1
  • System: Apple M1 Max
casesummaryops/sectime/opmarginsamples
@monstermann/babel-plugin-match๐Ÿฅ‡36M21nsยฑ0.09%47M
@monstermann/match-29%25M33nsยฑ0.05%30M
ts-pattern-88%4M279nsยฑ5.25%4M
shape (object expression)summaryops/sectime/opmarginsamples
@monstermann/babel-plugin-match๐Ÿฅ‡37M21nsยฑ0.05%49M
@monstermann/match-72%10M103nsยฑ0.56%10M
ts-pattern-98%597K2ยตsยฑ5.92%557K
shape (identifier)summaryops/sectime/opmarginsamples
@monstermann/babel-plugin-match๐Ÿฅ‡37M20nsยฑ0.08%49M
@monstermann/match-69%12M92nsยฑ1.13%11M
ts-pattern-98%591K2ยตsยฑ1.18%562K
condsummaryops/sectime/opmarginsamples
@monstermann/babel-plugin-match๐Ÿฅ‡34M22nsยฑ0.04%45M
@monstermann/match-27%24M36nsยฑ0.05%28M
ts-pattern-73%9M134nsยฑ6.87%7M

Installation โ€‹

sh
npm install @monstermann/match
npm install -D @monstermann/babel-plugin-match
sh
pnpm add @monstermann/match
pnpm add -D @monstermann/babel-plugin-match
sh
yarn add @monstermann/match
yarn add -D @monstermann/babel-plugin-match
sh
bun add @monstermann/match
bun add -D @monstermann/babel-plugin-match

Please consult your bundler of choice on how to enable Babel plugins, some examples:

vite + @vitejs/plugin-react
vite
ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig(() => ({
    plugins: [
        react({
            babel: {
                plugins: [["@monstermann/babel-plugin-match"]],
            },
        }),
    ],
}));
vite + vite-plugin-babel
ts
import { defineConfig } from "vite";
import babel from "vite-plugin-babel";

export default defineConfig({
    plugins: [babel()],
});
json
{
    "plugins": [["@monstermann/babel-plugin-match"]]
}

API โ€‹

returnType<T>() โ€‹

By default, the return type is inferred from your cases. You can enforce a specific type:

ts
match(foo)
    .returnType<"a" | "b" | "c">()
    .case(1, "a")
    .case(2, "b")
    .or("c")

.case(pattern, result) โ€‹

Matches a primitive value (===). Returns result if matched.

ts
const value = 2 as number

match(value)
    .case(1, "one")
    .case(2, "two")
    .case(3, "three")
    .orThrow() //=> "two"

.onCase(pattern, fn) โ€‹

Like .case, but calls fn(value) if matched. Useful for expensive computations.

ts
const value = 2 as number;

match(value)
    .onCase(1, (num) => num * -1)
    .onCase(2, (num) => num * -2)
    .onCase(3, (num) => num * -3)
    .orThrow(); //=> -4

.shape(object, result) โ€‹

Matches a shallow object shape. All fields must match (===), only supports matching primitives.

ts
type Rectangle = {
    x: number
    y: number
    width: number
    height: number
}

type Circle = {
    x: number
    y: number
    radius: number
}
    
const value: Rectangle = {
    x: 0,
    y: 0,
    width: 0,
    height: 0,
}

const isEmpty = match(value as Rectangle | Circle)
    .shape({ width: 0, height: 0 }, true)
    .shape({ radius: 0 }, true)
    .or(false) //=> true

.onShape(object, fn) โ€‹

Like .shape, but calls fn(value) if matched. Useful for expensive computations.

ts
type Rectangle = {
    kind: "rectangle"
    width: number
    height: number
}

type Circle = {
    kind: "circle"
    radius: number
}

const value: Rectangle = {
    kind: "rectangle",
    width: 10,
    height: 10,
}

const area = match(value as Rectangle | Circle)
    .shape({ kind: "rectangle" }, rect => rect.width * rect.height)
    .shape({ kind: "circle" }, circ => Math.PI * circ.radius ** 2)
    .orThrow() //=> 100

.cond(predicate, result) โ€‹

Matches if predicate(value) is truthy.

ts
match(10 as number)
    .cond((num) => num > 0, "positive")
    .cond((num) => num < 0, "negative")
    .or("zero"); //=> "positive"

.onCond(predicate, fn) โ€‹

Like .cond, but calls fn(value) if matched.

ts
match("Hello world!" as string)
    .onCond(msg => msg.length > 250, msg => `Message "${msg}" is too long`)
    .onCond(msg => msg.length < 100, msg => `Message "${msg}" is too short`)
    .or(false); //=> `Message "Hello world!" is too short`

.or(fallback) โ€‹

Returns the result, otherwise the given fallback.

ts
import { match } from "@monstermann/match";

match(3 as number)
    .case(1, "one")
    .case(2, "two")
    .or("other"); //=> "other"

.orElse(fn) โ€‹

Returns the result, otherwise calls fn(value).

ts
import { match } from "@monstermann/match";

match(3 as number)
    .case(1, "one")
    .case(2, "two")
    .orElse((num) => String(num)); //=> "3"

.orThrow() โ€‹

Returns the result, or throws an exception at runtime. Enforces exhaustiveness at compile time.

ts
import { match } from "@monstermann/match";

match(3 as number)
    .case(1, "one")
    .case(2, "two")
    .orThrow(); //=> Error
//   ~~~~~~~ โŒ Type 'MatchError<3>' has no call signatures.