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

quando

v0.1.6

Published

Typed pattern matching utility for TypeScript. Zero dependencies. Framework agnostic.

Readme

quando

Typed pattern matching utility for TypeScript. Zero dependencies. Framework agnostic.


Install

npm install quando
# or Bun
bun add quando

Overview

quando exports five complementary utilities:

| Export | Purpose | | -------------------- | ------------------------------------------------------------------------------------------------------------------- | | match(obj) | Chain-style pattern matcher against plain objects — great for composing class strings or deriving values from props | | when(value, ...) | Lightweight truthy branch helper — returns null on no-match, safe for JSX/template interpolation | | collect(...values) | Merges match() and when() results into a single space-separated string, filtering all falsy values | | each(items) | Svelte-style {#each} list helper — map items to output with an optional empty fallback | | resource(envelope) | Tri-state branch helper for async derived values (loading / error / ready) |


match()

Build a typed matcher against a plain object. Chain .when() calls to register matchers, then evaluate with .resolve(), .all(), .first(), or .last().

The default TOut is string, making it ergonomic for Tailwind / CSS-in-JS class composition without any type annotation.

Predicate form

import { match } from "quando";

const classes = match({ disabled: true })
  .when(({ disabled }) => disabled, "opacity-50 cursor-not-allowed")
  .resolve();
// → "opacity-50 cursor-not-allowed"

Key switch form

const classes = match({ variant: "primary" })
  .when("variant", {
    primary: "bg-indigo-600 text-white",
    secondary: "bg-slate-200 text-slate-900",
  })
  .resolve();
// → "bg-indigo-600 text-white"

Combining both — real-world button example

const classes = match({ variant: "primary", size: "lg", disabled: true })
  .when("variant", {
    primary: "bg-indigo-600 text-white",
    secondary: "bg-slate-200 text-slate-900",
  })
  .when("size", {
    sm: "px-2 py-1 text-sm",
    md: "px-4 py-2 text-base",
    lg: "px-6 py-3 text-lg",
  })
  .when(({ disabled }) => disabled, "opacity-50 cursor-not-allowed")
  .resolve();
// → "bg-indigo-600 text-white px-6 py-3 text-lg opacity-50 cursor-not-allowed"

Non-string values (explicit TOut)

import type { ReactNode } from "react";

const icon = match<typeof props, ReactNode>({ status: "error" })
  .when("status", {
    ok: <CheckIcon />,
    error: <XIcon />,
    pending: <SpinnerIcon />,
  })
  .first();

Result functions

Every case value can be a static result or a function that receives the full input — useful for derived values:

match({ size: "lg", scale: 4 })
  .when("size", {
    sm: ({ scale }) => `gap-${scale / 2}`,
    lg: ({ scale }) => `gap-${scale * 2}`,
  })
  .first();
// → "gap-8"

Terminal methods

| Method | Returns | Description | | ------------ | -------------------- | --------------------------------------------------------------------------- | | .resolve() | string | TOut[] | Joins all matched strings with a space; returns array for non-string TOut | | .all() | TOut[] | All matched results in registration order | | .first() | TOut \| null | First matched result — null when nothing matched (Ilha / JSX safe) | | .last() | TOut \| null | Last matched result — useful for override patterns |

Immutability — each .when() call returns a new builder. The original is never mutated.

Ilha islands

Pass snapshots into match() — read island state inside the object, not the accessor itself:

// ✅ snapshot fields
match({ variant: props.variant, count: state.todos().length })
  .when(({ count }) => count === 0, () => html`<p>No todos</p>`)
  .when("variant", { primary: () => html`<Badge />` })
  .first();

// ❌ accessors in the match object — match compares by value, not reactive paths
match({ todos: state.todos })

Button classes (pairs with Areia data-variant / collect):

<Button
  data-variant={collect(
    match({ active: isActive("/") })
      .when(({ active }) => active, "secondary")
      .resolve(),
    "ghost",
  )}
>
  Home
</Button>

Rules of thumb

| Goal | Use | | ---------------------------- | ---------------------------------------- | | Space-joined class string | .resolve() or collect(match(...).resolve(), …) | | One optional UI branch | .first() (returns null on no match) | | All matching branches | .all() | | Override / last wins | .last() |

Use .first() / .last() for JSX (like when()), not .resolve() — empty .resolve() is "", which is for strings.


when()

A truthy branch helper that returns null on no-match instead of false — because false renders as text in JSX/ilha templates while null is silently ignored.

Accepts any value — null, undefined, "", 0, and false are treated as no-match (same as if (value)). The true-branch callback receives the narrowed, truthy value.

Both branch callbacks receive the condition value as their argument. This keeps the API consistent and allows the callback to reference it without an outer closure.

Two-argument form — null on false

import { when } from "quando";

when(state.result(), (result) => <p>{result}</p>)
// → <p>…</p>  or  null

when(user.isPremium, () => <PremiumBadge />)
// → <PremiumBadge />  or  null

when(isActive, () => "ring-2 ring-indigo-500")
// → "ring-2 ring-indigo-500"  or  null

Three-argument form — explicit false branch

when(
  isOnline,
  () => "text-green-600",
  () => "text-red-500",
);
// → "text-green-600"  or  "text-red-500"

Both branches are lazy — the thunk for the untaken branch is never called.

The true and false branches may return different types:

when(
  flag,
  () => "active",
  () => 0,
);
// → string | number

JSX template pattern

<div class="card">
  {when(user.isPremium, () => (
    <PremiumBadge />
  ))}
  {when(
    count > 0,
    () => (
      <Counter value={count} />
    ),
    () => (
      <EmptyState />
    ),
  )}
</div>

collect()

Merges any number of string values into a single space-separated string, filtering out all falsy values (null, undefined, false, "").

Designed to compose match() and when() results in vanilla TS templates without reaching for an external utility like clsx.

import { collect } from "quando";

collect("px-4", null, "font-bold", undefined, "text-white");
// → "px-4 font-bold text-white"

collect(null, false, undefined, "");
// → ""

Composing with when() and match()

const classes = collect(
  match(props)
    .when("variant", { primary: "bg-indigo-600 text-white", secondary: "bg-slate-200" })
    .when("size", { sm: "px-2 py-1", md: "px-4 py-2", lg: "px-6 py-3" })
    .resolve(),
  when(props.disabled, () => "opacity-50 cursor-not-allowed"),
  when(props.active, () => "ring-2 ring-offset-2 ring-indigo-500"),
);
// → "bg-indigo-600 text-white px-6 py-3 opacity-50 cursor-not-allowed"

For JSX projects that already use clsx or cva, collect() is optional — those libraries own the same space. collect() shines in vanilla TS templates where no such utility is present.


each()

Svelte-style {#each} / {:else} for mapping collections to rendered output.

.as() returns a mapped array directly — empty collections render as []. Chain .else() only when you need an empty-state fallback.

Both branches are lazy — only the taken branch runs.

Basic list mapping

import { each } from "quando";

each([1, 2, 3]).as((n) => n * 2);
// → [2, 4, 6]

each(["a", "b"]).as((s, i) => `${i}:${s}`);
// → ["0:a", "1:b"]

each([]).as((n) => n * 2);
// → []

Use directly in ilha templates — arrays interpolate as concatenated children:

html`<ul>${each(items).as((item) => html`<li>${item.name}</li>`)}</ul>`

Empty fallback

each(items)
  .as((item) => html`<li>${item.name}</li>`)
  .else(() => html`<p>No items</p>`);

each(items)
  .as((item) => html`<li>${item.name}</li>`)
  .else(html`<p>No items</p>`);
// → RawHtml[]  or  RawHtml

When the collection is empty, .else() returns the fallback wrapped in a single-element array (unless the fallback is null or false) — a static value or the result of the callback (which receives the empty array). The map function is not called.

Keyed lists

Use .key() when rendering reorderable lists — the key is passed as the third argument to .as():

each(items)
  .key((item) => item.id)
  .as((item, index, id) => Row.key(id)({ item }))
  .else(() => html`<EmptyState />`);

API

| Call | Returns | Empty collection | | ---- | ------- | ---------------- | | .as(fn) | TOut[] | [] | | .as(fn).else(fn \| value) | TOut[] | TEmpty | fallback value or callback |


resource()

Tri-state branch helper for async derived envelopes — matches ilha's DerivedValue<T> shape ({ loading, value, error }).

Branch order is loading → error → ready. Only the taken branch runs.

Full tri-state

import { each, resource } from "quando";

resource(derived.users)
  .loading(() => html`<Spinner />`)
  .error((e) => html`<p>${e.message}</p>`)
  .ready((users) =>
    each(users ?? [])
      .key((u) => u.id)
      .as((u, _i, id) => Row.key(id)({ user: u }))
      .else(() => html`<EmptyState />`),
  );

Shorthand forms

Skip branches you don't need:

// error + ready only (no loading UI)
resource(derived.data)
  .error((e) => html`<Error message=${e.message} />`)
  .ready((data) => render(data));

// ready only
resource(derived.count).ready((n) => html`<span>${n ?? 0}</span>`);

When loading is true and no .loading() branch is registered, execution falls through to .ready() with value: undefined.


Ilha island example

Combining the full API in an island .render():

.render(({ derived, input }) =>
  html`<ul class="${collect(
    "list",
    when(input.compact, () => "list-compact"),
  )}">
    ${resource(derived.items)
      .loading(() => html`<li class="loading">Loading…</li>`)
      .error((e) => html`<li class="error">${e.message}</li>`)
      .ready((items) =>
        each(items ?? [])
          .key((item) => item.id)
          .as((item, _i, id) => Item.key(id)({ item }))
          .else(() => html`<li class="empty">Nothing here</li>`),
      )}
  </ul>`,
)

API reference

// Object pattern matching
match<TIn, TOut = string>(value: TIn): MatchBuilder<TIn, TOut>

// Truthy branching (null on no-match — Ilha / JSX safe)
when<T, R>(condition: T, onTrue: (value: NonFalsy<T>) => R): R | null
when<T, R, F>(condition: T, onTrue: (value: NonFalsy<T>) => R, onFalse: (value: T) => F): R | F
// MatchBuilder: .when(pred|key) → .resolve() | .all() | .first() | .last()
// .first() / .last() return null when nothing matches

// String merging
collect(...values: (string | null | undefined | false)[]): string

// List rendering
each<TItem>(items: readonly TItem[]): EachBuilder<TItem>
// EachBuilder: .key(fn) → .as(fn) → .else(fn)?
//              .as(fn)  → .else(fn)?
// .as() returns EachResult<TItem, TOut> (TOut[] with optional .else())

// Async derived tri-state
resource<T>(envelope: ResourceEnvelope<T>): ResourceBuilder<T>
// ResourceEnvelope: { loading: boolean; value: T | undefined; error: Error | undefined }
// ResourceBuilder: .loading(fn) → .error(fn) → .ready(fn)
//                  .error(fn) → .ready(fn)
//                  .ready(fn)

Design principles

  • Zero dependencies — ships nothing but TypeScript source.
  • Immutable builders.when() always returns a new builder; safe to share and reuse intermediate chains.
  • Lazy evaluation — result functions and thunks are only called when their branch is taken.
  • null not falsewhen() and match().first() / .last() follow JSX conventions so no-match renders are silent.
  • Value threadingwhen() passes the condition into both branch callbacks, keeping logic self-contained without outer closures.
  • Composable by design — all exports are independent but built to work together.
  • Framework agnostic — works equally in React, Preact, Solid, Svelte, Ilha, or plain TS.
  • Svelte-familiar control floweach().as().else() mirrors {#each} / {:else}; resource() handles async derived envelopes.

License

MIT