@mcabreradev/filter
v5.10.1
Published
A powerful, SQL-like array filtering library for TypeScript and JavaScript with advanced pattern matching, MongoDB-style operators, deep object comparison, and zero dependencies
Downloads
577
Maintainers
Keywords
Readme
@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.
Table of Contents
- The Problem
- Quick Start
- Why You'll Love It
- Examples
- Framework Integrations
- Core Features
- Advanced Features
- Documentation
- Performance
- Bundle Size
- Browser Support
- Migration from v3.x
- Changelog
- Contributing
- License
- Support
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/filterRequirements: 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', ... }]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 allocatedReal-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
isPendingstate - ✅ Pagination hook with
nextPage,prevPage,goToPage - ✅ SSR compatible
- ✅ 100% test coverage
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 | ❌ | ✅ |
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.
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]Documentation
📖 Complete Guides
- Getting Started — Installation and first steps
- All Operators — Complete operator reference
- Geospatial Queries — Location-based filtering
- Datetime Operators — Temporal filtering
- Framework Integrations — React, Vue, Svelte, Angular, SolidJS, Preact
- Lazy Evaluation — Efficient large dataset processing
- Memoization & Caching — Performance optimization
- Visual Debugging — Debug mode and tree visualization
🎯 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 });Changelog
v5.9.2 (Current)
- 🧹 Refactor: Removed Svelte integration exports, dependencies, and related docs from the package.
- 🔧 CI/CD: Hardened npm publish workflow with safer version resolution and improved release handling.
- 📦 Release Reliability: Synced lockfile with dependency changes to avoid CI failures with
--frozen-lockfile. - ✅ Publishing: Improved release pipeline behavior so versioning and GitHub Releases run more consistently.
v5.9.1
- 🚀 Release: Published patch release with workflow and release-process fixes.
- 🏷️ Versioning: Stabilized tag/version flow to avoid collisions with existing release tags.
v5.8.2
- 🐛 Bug Fix: Wildcard regex now correctly escapes all special characters (
.,+,*,?,(,[,^, etc.) — patterns like%.txtora.b%no longer silently break - 🐛 Bug Fix:
$timeOfDaywithstart > end(e.g.{ start: 22, end: 5 }) now correctly fails validation instead of silently never matching - 🐛 Bug Fix: React
useDebouncedFilternow reacts todelayprop changes — previously the initial delay was frozen for the hook's lifetime - 🔒 Validation:
limitoption now validated by schema — negative or non-integer values throw a clear configuration error - 🔒 Validation:
debug,verbose,showTimings,colorize,enablePerformanceMonitoringoptions now validated by schema - ⚡ Performance: Pattern-matching regex cache now delegates to the shared LRU
MemoizationManager— the previously unboundedMapis 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 idiomaticget()fromsvelte/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
limitconfiguration 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
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
