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
- @mcabreradev/filter
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.
npm install @mcabreradev/filter
# or
pnpm add @mcabreradev/filter
# or
yarn add @mcabreradev/filterRequirements: Node.js >= 20, TypeScript 5.0+ (optional)
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 โ
- 530x faster on repeated queries with optional LRU caching
- 500x faster with lazy evaluation for large datasets
- Compiled predicates and regex patterns cached automatically
- Intuitive API โ reads like English
- SQL-like wildcards (
%,_) you already know - Full TypeScript generics with intelligent autocomplete
- Four filtering strategies: strings, objects, operators, predicates
- Combine them seamlessly in a single expression
- Works with any data shape โ flat, nested, arrays
- 1,004+ tests ensuring bulletproof reliability
- Zero runtime dependencies (only Zod for optional validation)
- Battle-tested in production applications
- MIT licensed
- Full package: 12KB gzipped
- Core only: 8.4KB gzipped
- Zero mandatory dependencies
- Tree-shakeable โ only pay for what you use
- Built with strict TypeScript
- Catch errors at compile time, not runtime
- Full IntelliSense for operators based on field types
- 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
- 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
// 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);// 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' } });// 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']
});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 }
}
}
});// 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') } });// 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 allocatedinterface 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
});First-class hooks and composables โ reactive, debounced, paginated, ready to drop in:
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} />)}
</>
);
}<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><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}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>);
}import { useFilter } from '@mcabreradev/filter/solidjs';
function UserList() {
const { filtered } = useFilter(
() => users,
() => ({ active: true })
);
return <For each={filtered()}>{(u) => <div>{u.name}</div>}</For>;
}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
๐ Complete Framework Guide โ
| 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.
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
});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
});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 โ
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.
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 โ
- 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
| 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);| Import | Size (gzipped) | Tree-Shakeable |
|---|---|---|
| Full | 12 KB | โ |
| Core only | 8.4 KB | โ |
| React hooks | 9.2 KB | โ |
| Lazy evaluation | 5.4 KB | โ |
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
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 โ
- ๐ 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
- ๐จ 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
๐ ฐ๏ธ Angular: Services and Pipes with Signals support- ๐ท SolidJS: Signal-based reactive hooks
- โก Preact: Lightweight hooks API
- ๐ Geospatial Operators: Location-based filtering with
$near,$geoBox,$geoPolygon - ๐
Datetime Operators: Temporal filtering with
$recent,$upcoming,$dayOfWeek,$age
- ๐จ 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 โ
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
MIT License โ see LICENSE.md for details.
Copyright (c) 2025 Miguelangel Cabrera
- ๐ Complete Documentation
- ๐ฌ GitHub Discussions
- ๐ Issue Tracker
- โญ Star on GitHub
Made with โค๏ธ for the JavaScript/TypeScript community