Skip to content

mcabreradev/filter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

223 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

@mcabreradev/filter

Filter arrays like a pro. A powerful, SQL-like array filtering library for TypeScript with advanced pattern matching, MongoDB-style operators, deep object comparison, geospatial queries, and zero dependencies.

Quick Start โ€ข Why You'll Love It โ€ข Examples โ€ข Playground โ€ข Documentation


Table of Contents


The Problem

Tired of writing complex filter logic? Stop wrestling with nested Array.filter() chains and verbose conditionals. Write clean, declarative filters that read like queries.

Before โ€” the usual mess:

const results = data.filter(item =>
  item.age >= 18 &&
  item.status === 'active' &&
  (item.role === 'admin' || item.role === 'moderator') &&
  item.email.endsWith('@company.com') &&
  item.createdAt >= thirtyDaysAgo
);

After โ€” clean and declarative:

const results = filter(data, {
  age: { $gte: 18 },
  status: 'active',
  role: ['admin', 'moderator'],
  email: { $endsWith: '@company.com' },
  createdAt: { $gte: thirtyDaysAgo }
});

Same result. 70% less code. 100% more readable.


Quick Start

Install

npm install @mcabreradev/filter
# or
pnpm add @mcabreradev/filter
# or
yarn add @mcabreradev/filter

Requirements: Node.js >= 20, TypeScript 5.0+ (optional)

Your First Filter

import { filter } from '@mcabreradev/filter';

const users = [
  { name: 'Alice', age: 30, city: 'Berlin', active: true },
  { name: 'Bob', age: 25, city: 'London', active: false },
  { name: 'Charlie', age: 35, city: 'Berlin', active: true }
];

// Simple string search โ€” scans all fields
const berlinUsers = filter(users, 'Berlin');
// โ†’ [{ name: 'Alice', ... }, { name: 'Charlie', ... }]

// Object matching โ€” AND logic across fields
const activeBerlinUsers = filter(users, { city: 'Berlin', active: true });
// โ†’ [{ name: 'Alice', ... }]

// MongoDB-style operators
const adults = filter(users, { age: { $gte: 18 } });
// โ†’ All users

// SQL-like wildcards
const startsWithAl = filter(users, 'Al%');
// โ†’ [{ name: 'Alice', ... }]

๐ŸŽฎ Try it in the Playground โ†’


Why You'll Love It

๐Ÿš€ Blazing Fast

  • 530x faster on repeated queries with optional LRU caching
  • 500x faster with lazy evaluation for large datasets
  • Compiled predicates and regex patterns cached automatically

๐ŸŽฏ Developer Friendly

  • Intuitive API โ€” reads like English
  • SQL-like wildcards (%, _) you already know
  • Full TypeScript generics with intelligent autocomplete

๐Ÿ”ง Incredibly Flexible

  • Four filtering strategies: strings, objects, operators, predicates
  • Combine them seamlessly in a single expression
  • Works with any data shape โ€” flat, nested, arrays

๐Ÿ“ฆ Production Ready

  • 1,004+ tests ensuring bulletproof reliability
  • Zero runtime dependencies (only Zod for optional validation)
  • Battle-tested in production applications
  • MIT licensed

๐Ÿชถ Ultra Lightweight

  • Full package: 12KB gzipped
  • Core only: 8.4KB gzipped
  • Zero mandatory dependencies
  • Tree-shakeable โ€” only pay for what you use

๐Ÿ”’ Type-Safe by Default

  • Built with strict TypeScript
  • Catch errors at compile time, not runtime
  • Full IntelliSense for operators based on field types

๐ŸŽจ Framework Agnostic

  • First-class hooks: React, Vue, Svelte, Angular, SolidJS, Preact
  • Debounced search, pagination, and reactive state out of the box
  • SSR compatible: Next.js, Nuxt, SvelteKit

๐Ÿ“Š Handles Big Data

  • Generator-based lazy evaluation for millions of records
  • Early exit โ€” stop processing when you have enough results
  • LRU caches with TTL prevent memory leaks in long-running apps

Examples

Basic Filtering

// String matching โ€” searches all string properties
filter(products, 'Laptop');

// Exact field matching โ€” AND logic
filter(products, { category: 'Electronics', price: { $lt: 1000 } });

// SQL wildcard patterns
filter(users, '%alice%');   // contains 'alice'
filter(users, 'Al%');       // starts with 'Al'
filter(users, '%son');      // ends with 'son'
filter(users, 'J_hn');      // single-char wildcard

// Predicate functions โ€” full control
filter(users, (u) => u.score > 90 && u.verified);

MongoDB-Style Operators

// Comparison
filter(products, { price: { $gte: 100, $lte: 500 } });
filter(products, { rating: { $gt: 4 }, stock: { $ne: 0 } });

// Array membership
filter(products, { category: { $in: ['Electronics', 'Books'] } });
filter(products, { tags: { $contains: 'sale' } });
filter(products, { sizes: { $size: 3 } });

// String matching
filter(users, {
  email: { $endsWith: '@company.com' },
  name: { $startsWith: 'John' },
  bio: { $regex: /developer/i }
});

// Logical combinators
filter(products, {
  $and: [
    { inStock: true },
    { $or: [{ rating: { $gte: 4.5 } }, { price: { $lt: 50 } }] }
  ]
});

// Negate with $not
filter(users, { role: { $not: 'banned' } });

Array OR Syntax (Intuitive!)

// Pass an array โ†’ automatic OR logic, no $in needed
filter(products, { category: ['Electronics', 'Books'] });
// Same as: { category: { $in: ['Electronics', 'Books'] } }

// Combine across fields
filter(users, {
  city: ['Berlin', 'Paris', 'London'],
  role: ['admin', 'moderator']
});

Geospatial Queries

import { filter, type GeoPoint } from '@mcabreradev/filter';

const userLocation: GeoPoint = { lat: 52.52, lng: 13.405 };

// Find restaurants within 5km rated 4.5+
filter(restaurants, {
  location: { $near: { center: userLocation, maxDistanceMeters: 5000 } },
  rating: { $gte: 4.5 }
});

// Bounding box search
filter(places, {
  location: {
    $geoBox: {
      topLeft: { lat: 53.0, lng: 13.0 },
      bottomRight: { lat: 52.0, lng: 14.0 }
    }
  }
});

Datetime Filtering

// Events in next 7 days
filter(events, { date: { $upcoming: { days: 7 } } });

// Recent activity (last 24 hours)
filter(logs, { createdAt: { $recent: { hours: 24 } } });

// Weekday events during business hours
filter(events, {
  date: { $dayOfWeek: [1, 2, 3, 4, 5] },       // Monโ€“Fri
  startTime: { $timeOfDay: { start: 9, end: 17 } }  // 9amโ€“5pm
});

// Users of age 18โ€“65
filter(users, { birthDate: { $age: { min: 18, max: 65 } } });

// Weekend-only events
filter(events, { date: { $isWeekend: true } });

// Events before a deadline
filter(tasks, { dueDate: { $isBefore: new Date('2025-12-31') } });

Performance Optimization

// LRU caching โ€” 530x faster on repeat queries
const results = filter(largeDataset, expression, {
  enableCache: true,
  orderBy: { field: 'price', direction: 'desc' },
  limit: 100
});

// Lazy evaluation โ€” process millions of records without loading all into memory
import { filterFirst, filterExists, filterCount, filterLazy } from '@mcabreradev/filter';

const first10 = filterFirst(millionRecords, { premium: true }, 10);
const hasAdmin = filterExists(users, { role: 'admin' });      // exits on first match
const activeCount = filterCount(users, { active: true });     // no array allocated

Real-World: E-commerce Search

interface Product {
  id: number;
  name: string;
  price: number;
  category: string;
  brand: string;
  rating: number;
  inStock: boolean;
  tags: string[];
}

// Affordable, highly-rated electronics in stock
const results = filter<Product>(products, {
  category: 'Electronics',
  price: { $lte: 1000 },
  rating: { $gte: 4.5 },
  inStock: true
});

// Full-text search with brand filter
const searchResults = filter<Product>(products, {
  name: { $contains: 'laptop' },
  brand: ['Apple', 'Dell', 'HP'],
  price: { $gte: 500, $lte: 2000 }
});

// Sorted and paginated results
const page1 = filter<Product>(products, { category: 'Electronics', inStock: true }, {
  orderBy: [
    { field: 'price', direction: 'asc' },
    { field: 'rating', direction: 'desc' }
  ],
  limit: 20
});

Framework Integrations

First-class hooks and composables โ€” reactive, debounced, paginated, ready to drop in:

React

import { useFilter, useDebouncedFilter, usePaginatedFilter } from '@mcabreradev/filter/react';

function UserList() {
  const { filtered, isFiltering } = useFilter(users, { active: true });

  return (
    <ul>
      {filtered.map(u => <li key={u.id}>{u.name}</li>)}
    </ul>
  );
}

// Debounced live search
function SearchBox() {
  const [query, setQuery] = useState('');
  const { filtered, isPending } = useDebouncedFilter(users, query, { delay: 300 });

  return (
    <>
      <input onChange={e => setQuery(e.target.value)} />
      {isPending ? <Spinner /> : filtered.map(u => <User key={u.id} user={u} />)}
    </>
  );
}

Vue

<script setup lang="ts">
import { ref } from 'vue';
import { useFilter } from '@mcabreradev/filter/vue';

const expression = ref({ active: true });
const { filtered, isFiltering } = useFilter(users, expression);
</script>

<template>
  <ul>
    <li v-for="user in filtered" :key="user.id">{{ user.name }}</li>
  </ul>
</template>

Svelte

<script lang="ts">
  import { writable } from 'svelte/store';
  import { useFilter } from '@mcabreradev/filter/svelte';

  const expression = writable({ active: true });
  const { filtered } = useFilter(users, expression);
</script>

{#each $filtered as user}
  <p>{user.name}</p>
{/each}

Angular

import { FilterService } from '@mcabreradev/filter/angular';

@Component({
  providers: [FilterService],
  template: `
    @for (user of filterService.filtered(); track user.id) {
      <div>{{ user.name }}</div>
    }
  `
})
export class UserListComponent {
  filterService = inject(FilterService<User>);
}

SolidJS

import { useFilter } from '@mcabreradev/filter/solidjs';

function UserList() {
  const { filtered } = useFilter(
    () => users,
    () => ({ active: true })
  );
  return <For each={filtered()}>{(u) => <div>{u.name}</div>}</For>;
}

Preact

import { useFilter } from '@mcabreradev/filter/preact';

function UserList() {
  const { filtered } = useFilter(users, { active: true });
  return <div>{filtered.map(u => <div key={u.id}>{u.name}</div>)}</div>;
}

Every integration includes:

  • โœ… Full TypeScript generics
  • โœ… Debounced search hook with isPending state
  • โœ… Pagination hook with nextPage, prevPage, goToPage
  • โœ… SSR compatible
  • โœ… 100% test coverage

๐Ÿ“– Complete Framework Guide โ†’


Core Features

Supported Operators

Category Operators
Comparison $gt $gte $lt $lte $eq $ne
Array $in $nin $contains $size
String $startsWith $endsWith $contains $regex $match
Logical $and $or $not
Geospatial $near $geoBox $geoPolygon
Datetime $recent $upcoming $dayOfWeek $timeOfDay $age $isWeekday $isWeekend $isBefore $isAfter

18+ operators covering every filtering scenario you'll encounter.

TypeScript Support

Full type safety โ€” autocomplete shows only valid operators for each field type:

interface Product {
  name: string;
  price: number;
  tags: string[];
}

filter<Product>(products, {
  price: { $gte: 100 },    // โœ… number operators
  name: { $contains: '' }, // โœ… string operators
  tags: { $size: 3 },      // โœ… array operators
  price: { $contains: '' } // โŒ TypeScript error โ€” string op on number field
});

Configuration Options

filter(data, expression, {
  caseSensitive: false,           // default: false
  maxDepth: 3,                    // nested object traversal depth (1โ€“10)
  enableCache: true,              // LRU result caching (530x speedup)
  orderBy: 'price',               // sort field or array of fields
  limit: 10,                      // cap result count
  debug: true,                    // print expression tree to console
  verbose: true,                  // detailed per-item evaluation logs
  showTimings: true,              // execution time per operator
  enablePerformanceMonitoring: true,  // collect performance metrics
});

Advanced Features

Lazy Evaluation

Process large datasets without loading everything into memory:

import { filterLazy, filterFirst, filterExists, filterCount } from '@mcabreradev/filter';

// Generator โ€” pull items one by one, exit any time
const lazy = filterLazy(millionRecords, { active: true });
for (const item of lazy) {
  process(item);
  if (shouldStop) break; // โ† zero wasted work
}

// Grab first N matches
const top10 = filterFirst(users, { premium: true }, 10);

// Check existence โ€” exits on first match
const hasBanned = filterExists(users, { role: 'banned' });

// Count matches โ€” no array allocated
const total = filterCount(orders, { status: 'pending' });
Scenario Array.filter filterLazy / filterFirst
First match in 1M items ~50ms ~0.1ms
Memory for 1M items ~80MB ~0KB
Early exit โŒ โœ…

๐Ÿ“– Lazy Evaluation Guide โ†’

Memoization & Caching

Three-tier LRU caching strategy with automatic TTL eviction:

// First call โ€” compiles predicates, runs filter, stores result
const results = filter(largeDataset, { age: { $gte: 18 } }, { enableCache: true });

// Subsequent calls โ€” returns cached result instantly
const same = filter(largeDataset, { age: { $gte: 18 } }, { enableCache: true });
Scenario Without Cache With Cache Speedup
Simple query, 10K items 5.3ms 0.01ms 530x
Regex pattern 12.1ms 0.02ms 605x
Complex nested query 15.2ms 0.01ms 1520x

Caches are bounded (LRU, max 500 entries each) and auto-expire after 5 minutes โ€” safe for long-running servers.

๐Ÿ“– Memoization Guide โ†’

Visual Debugging

Built-in tree visualization for understanding filter behavior:

filter(users, { city: 'Berlin', age: { $gte: 18 } }, { debug: true });

// Console output:
// โ”Œโ”€ Filter Debug Tree
// โ”‚  Expression: {"city":"Berlin","age":{"$gte":18}}
// โ”‚  Matched: 3/10 items (30.0%)
// โ”‚  Execution time: 0.42ms
// โ”œโ”€ โœ“ city = "Berlin"         [3 matches]
// โ””โ”€ โœ“ age >= 18               [3 matches]

๐Ÿ“– Debug Guide โ†’


Documentation

๐Ÿ“– Complete Guides

๐ŸŽฏ Quick Links


Performance

Technique Benefit
Early-exit operators Skip remaining items on first mismatch
LRU result cache 530xโ€“1520x speedup on repeated queries
LRU predicate cache Compiled predicates reused across calls
LRU regex cache Compiled patterns reused, bounded to 500 entries
Lazy generators 500x faster when you don't need all results
Absolute TTL eviction Stale entries removed after 5 min โ€” no memory leaks
// Enable all optimizations at once
filter(data, expression, { enableCache: true });

// Maximum efficiency for large datasets
const first100 = filterFirst(millionRecords, { active: true }, 100);

Bundle Size

Import Size (gzipped) Tree-Shakeable
Full 12 KB โœ…
Core only 8.4 KB โœ…
React hooks 9.2 KB โœ…
Lazy evaluation 5.4 KB โœ…

Browser Support

Works in all modern browsers and Node.js:

  • Node.js: >= 20
  • Browsers: Chrome, Firefox, Safari, Edge (latest versions)
  • TypeScript: >= 5.0
  • Module Systems: ESM, CommonJS

Migration from v3.x

Good news: v5.x is 100% backward compatible. All v3.x code continues to work.

// โœ… All v3.x syntax still works
filter(data, 'string');
filter(data, { prop: 'value' });
filter(data, (item) => true);
filter(data, '%pattern%');

// โœ… New in v5.x
filter(data, { age: { $gte: 18 } });
filter(data, expression, { enableCache: true, limit: 50 });

๐Ÿ“– Migration Guide โ†’


Changelog

v5.8.2 (Current)

  • ๐Ÿ› Bug Fix: Wildcard regex now correctly escapes all special characters (., +, *, ?, (, [, ^, etc.) โ€” patterns like %.txt or a.b% no longer silently break
  • ๐Ÿ› Bug Fix: $timeOfDay with start > end (e.g. { start: 22, end: 5 }) now correctly fails validation instead of silently never matching
  • ๐Ÿ› Bug Fix: React useDebouncedFilter now reacts to delay prop changes โ€” previously the initial delay was frozen for the hook's lifetime
  • ๐Ÿ”’ Validation: limit option now validated by schema โ€” negative or non-integer values throw a clear configuration error
  • ๐Ÿ”’ Validation: debug, verbose, showTimings, colorize, enablePerformanceMonitoring options now validated by schema
  • โšก Performance: Pattern-matching regex cache now delegates to the shared LRU MemoizationManager โ€” the previously unbounded Map is gone
  • โšก Performance: LRU cache TTL is now absolute (expire 5 min after creation) instead of sliding โ€” entries can no longer live forever under heavy load
  • ๐Ÿงน Code Quality: Svelte pagination replaced subscribe()() anti-pattern with idiomatic get() from svelte/store
  • โœ… Tests: 1,004+ tests โ€” added coverage for every bug fixed in this release

v5.8.0

  • ๐ŸŽจ New Framework Integrations: Angular, SolidJS, and Preact support
  • ๐Ÿ”ข Limit Option: New limit configuration to restrict result count
  • ๐Ÿ“Š OrderBy Option: Sort filtered results by field(s) in ascending or descending order
  • โœ… 993+ tests with comprehensive coverage

v5.7.0

  • ๐Ÿ…ฐ๏ธ Angular: Services and Pipes with Signals support
  • ๐Ÿ”ท SolidJS: Signal-based reactive hooks
  • โšก Preact: Lightweight hooks API

v5.6.0

  • ๐ŸŒ Geospatial Operators: Location-based filtering with $near, $geoBox, $geoPolygon
  • ๐Ÿ“… Datetime Operators: Temporal filtering with $recent, $upcoming, $dayOfWeek, $age

v5.5.0

  • ๐ŸŽจ Array OR Syntax: Intuitive array-based OR filtering
  • ๐Ÿ› Visual Debugging: Built-in debug mode with expression tree visualization
  • ๐ŸŽฎ Interactive Playground: Online playground for testing filters

๐Ÿ“– Full Changelog โ†’


Contributing

We welcome contributions! Please read our Contributing Guide for details.

Ways to Contribute:

  • Report bugs or request features via GitHub Issues
  • Submit pull requests with bug fixes or new features
  • Improve documentation
  • Share your use cases and examples

License

MIT License โ€” see LICENSE.md for details.

Copyright (c) 2025 Miguelangel Cabrera


Support


Made with โค๏ธ for the JavaScript/TypeScript community

About

A powerful, zero-dependency filtering library that brings MongoDB-style operators, SQL wildcards, and intelligent autocomplete to TypeScript arrays. Think of it as Array.filter() on steroids! ๐Ÿ’ช

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Contributors