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

qmatch

v1.0.3

Published

Type-safe criteria matching for TypeScript. MongoDB-style query syntax with full autocomplete.

Readme

qmatch

Type-safe criteria matching for TypeScript. Filter objects with MongoDB-style queries and full autocomplete.

Installation

npm install qmatch
# or
pnpm add qmatch
# or
yarn add qmatch

Quick Start

import { match } from 'qmatch';

const isEligible = match<User>({
  age: { $gte: 18 },
  status: { $in: ['active', 'pending'] },
  profile: {
    verified: true,
  },
});

users.filter(isEligible);

Features

  • Full TypeScript support - Operators constrained by field type, autocomplete at every level
  • Nested object queries - Zod/Prisma-style nesting with dot-path error reporting
  • Composable logic - $and, $or, $not for complex criteria
  • Array.filter() compatible - Returns predicate function (item: T) => boolean
  • Debuggable - Built-in explain() to see why items don't match

Usage

Basic Matching

import { match } from 'qmatch';

// Simple equality (implicit $eq)
const isActive = match<User>({ status: 'active' });

// Comparison operators
const adults = match<User>({ age: { $gte: 18 } });

// Multiple conditions (implicit AND)
const qualifiedLeads = match<Lead>({
  requestAmount: { $gte: 15_000 },
  loanPurpose: { $in: ['DEBT_CONSOLIDATION', 'CREDIT_CARD'] },
});

// Use with filter
const results = leads.filter(qualifiedLeads);

Nested Objects

Queries support deep nesting with full autocomplete:

const premiumArtist = match<Song>({
  album: {
    artist: {
      verified: true,
      monthlyListeners: { $gte: 1_000_000 },
    },
  },
});

TypeScript catches errors at compile time:

// ❌ Error: 'veriified' does not exist on type...
match<Song>({
  album: {
    artist: {
      veriified: true,  // Typo caught!
    },
  },
});

Logical Operators

// $or - any condition matches
const highValueOrVerified = match<Lead>({
  $or: [
    { requestAmount: { $gte: 50_000 } },
    { profile: { verified: true } },
  ],
});

// $and - explicit AND (useful for nested logic)
const complex = match<Lead>({
  $and: [
    { status: 'active' },
    { $or: [
      { tier: 'premium' },
      { score: { $gte: 800 } },
    ]},
  ],
});

// $not - negation
const notPending = match<User>({
  $not: { status: 'pending' },
});

Custom Functions

// Field-level custom logic
const roundNumbers = match<Lead>({
  requestAmount: { $fn: (amt) => amt % 1000 === 0 },
});

// Query-level custom logic
const lowDTI = match<Lead>({
  $where: (lead) => {
    const dti = lead.monthlyDebt / (lead.annualIncome / 12);
    return dti < 0.43;
  },
});

// Combine with other operators
const qualified = match<Lead>({
  requestAmount: { $gte: 10_000 },
  $where: (lead) => calculateRiskScore(lead) < 50,
});

Operators

Comparison (number | Date)

| Operator | Description | |----------|-------------| | $gt | Greater than | | $gte | Greater than or equal | | $lt | Less than | | $lte | Less than or equal |

match<Event>({
  date: { $gte: new Date('2024-01-01'), $lt: new Date('2025-01-01') },
  attendees: { $gt: 100 },
});

Equality (all types)

| Operator | Description | |----------|-------------| | $eq | Equal (implicit when passing direct value) | | $ne | Not equal | | $in | Value in array | | $nin | Value not in array |

match<User>({
  role: { $in: ['admin', 'moderator'] },
  status: { $ne: 'banned' },
});

String

| Operator | Description | |----------|-------------| | $regex | RegExp or string pattern |

match<User>({
  email: { $regex: /@gmail\.com$/i },
});

Array

| Operator | Description | |----------|-------------| | $contains | Array includes value | | $size | Array length equals |

match<Artist>({
  genres: { $contains: 'rock', $size: 3 },
});

Existence

| Operator | Description | |----------|-------------| | $exists | Field is not null/undefined |

match<Lead>({
  referralCode: { $exists: true },
  deletedAt: { $exists: false },
});

Logical

| Operator | Description | |----------|-------------| | $and | All conditions must match | | $or | Any condition must match | | $not | Negates a query | | $where | Custom predicate function |

Debugging with explain()

When a match fails, use explain() to see why:

const isEligible = match<Lead>({
  score: { $gte: 700 },
  income: { $gte: 50_000 },
});

if (!isEligible(lead)) {
  const result = isEligible.explain(lead);
  console.log(result.failure?.message);
  // "score: $gte expected >= 700, got 650"
}

The ExplainResult contains:

interface ExplainResult {
  matched: boolean;
  failure?: {
    path: string;      // "profile.address.zipCode"
    operator: string;  // "$gte"
    expected: unknown; // ">= 700"
    actual: unknown;   // 650
    message: string;   // Human-readable summary
  };
}

Standalone explain()

You can also use explain() directly with a query:

import { explain, type Query } from 'qmatch';

const query: Query<Lead> = {
  status: 'active',
  score: { $gte: 700 },
};

const result = explain(query, lead);

Type Safety

Operators are constrained by field type:

interface User {
  name: string;
  age: number;
  createdAt: Date;
  tags: string[];
}

// ✅ Valid - $gte works on numbers
match<User>({ age: { $gte: 18 } });

// ✅ Valid - $regex works on strings
match<User>({ name: { $regex: /^A/ } });

// ✅ Valid - $contains works on arrays
match<User>({ tags: { $contains: 'premium' } });

// ❌ Type error - $gte doesn't work on strings
match<User>({ name: { $gte: 'A' } });

// ❌ Type error - $regex doesn't work on numbers
match<User>({ age: { $regex: /\d+/ } });

// ❌ Type error - wrong type for $in
match<User>({ age: { $in: ['18', '21'] } });  // Should be number[]

API Reference

match<T>(query: Query<T>): Matcher<T>

Creates a matcher function from a query.

const matcher = match<User>({ status: 'active' });
matcher(user);           // boolean
matcher.explain(user);   // ExplainResult

explain<T>(query: Query<T>, item: T): ExplainResult

Standalone function to explain why an item matches or doesn't match.

const result = explain({ status: 'active' }, user);

Types

import type { Query, Matcher, ExplainResult } from 'qmatch';

// Define queries with full type checking
const query: Query<User> = { ... };

// Matcher type for function signatures
function filterUsers(users: User[], matcher: Matcher<User>): User[] {
  return users.filter(matcher);
}

Edge Cases

| Scenario | Behavior | |----------|----------| | Empty query {} | Matches everything | | Empty $or: [] | Matches nothing | | Empty $and: [] | Matches everything | | Null nested object | Fails unless $exists: false | | Comparison on null | Returns false |

Contributing

Contributions are welcome! Here's how to get started:

Setup

git clone https://github.com/rkingon/qmatch.git
cd qmatch
pnpm install

Development

pnpm test        # Run tests
pnpm test:watch  # Run tests in watch mode
pnpm typecheck   # Type check
pnpm build       # Build the package

Making Changes

This project uses Changesets for version management.

  1. Create a branch for your changes
  2. Make your changes
  3. Add a changeset describing your changes:
    pnpm changeset
  4. Select the change type:
    • patch - Bug fixes, documentation updates
    • minor - New features (backwards compatible)
    • major - Breaking changes
  5. Write a summary of your changes
  6. Commit the changeset file along with your changes
  7. Open a pull request

When your PR is merged, the release workflow will automatically create a "Version Packages" PR. Once that's merged, the package is published to npm.

License

MIT