The in-memory adapter transforms filter definitions into predicate functions that work seamlessly with native JavaScript array methods like filter(), find(), some(), and every().
Installation
Install the package
npm install @filter-def/in-memory
# or
pnpm add @filter-def/in-memory
Basic Usage
Define your filters once, then use them with standard array methods:
import { inMemoryFilter } from "@filter-def/in-memory" ;
import type { InMemoryFilterInput } from "@filter-def/in-memory" ;
interface Product {
id : string ;
name : string ;
price : number ;
category : string ;
inStock : boolean ;
}
const productFilter = inMemoryFilter < Product >(). def ({
// Field names match entity properties, so `field` is inferred
id: { kind: "eq" },
category: { kind: "eq" },
inStock: { kind: "eq" },
// Explicit field names when filter key differs
nameContains: { kind: "contains" , field: "name" },
minPrice: { kind: "gte" , field: "price" },
maxPrice: { kind: "lte" , field: "price" },
});
const products : Product [] = [
{ id: "1" , name: "Laptop" , price: 999 , category: "electronics" , inStock: true },
{ id: "2" , name: "Phone" , price: 699 , category: "electronics" , inStock: true },
{ id: "3" , name: "Desk" , price: 299 , category: "furniture" , inStock: false },
];
// Create a predicate function
const predicate = productFilter ({
category: "electronics" ,
inStock: true ,
maxPrice: 800 ,
});
// Use with array methods
const results = products . filter ( predicate );
const first = products . find ( predicate );
const hasAny = products . some ( predicate );
Filter Types
The in-memory adapter supports all standard filter kinds:
Kind Description Example eqExact equality { name: { kind: "eq" } }neqNot equal { status: { kind: "neq" } }containsString contains (case-sensitive by default) { email: { kind: "contains" } }inArrayValue in array { status: { kind: "inArray" } }gtGreater than { age: { kind: "gt" } }gteGreater than or equal { price: { kind: "gte" } }ltLess than { age: { kind: "lt" } }lteLess than or equal { price: { kind: "lte" } }isNullCheck if null/undefined { deletedAt: { kind: "isNull" } }isNotNullCheck if not null/undefined { email: { kind: "isNotNull" } }
Case-Insensitive String Matching
Use caseInsensitive: true for case-insensitive string matching:
const userFilter = inMemoryFilter < User >(). def ({
nameSearch: {
kind: "contains" ,
field: "name" ,
caseInsensitive: true ,
},
});
const users = [
{ name: "Alice" },
{ name: "Bob" },
{ name: "CHARLIE" },
];
// Matches "Alice", "CHARLIE"
const results = users . filter ( userFilter ({ nameSearch: "ali" }));
Nested Fields
Filter on deeply nested object properties using dot-separated paths:
interface Employee {
name : { first : string ; last : string };
department : string ;
address : { city : string ; geo : { lat : number ; lng : number } };
}
const employeeFilter = inMemoryFilter < Employee >(). def ({
firstName: { kind: "eq" , field: "name.first" },
lastName: { kind: "eq" , field: "name.last" },
department: { kind: "eq" },
cityContains: {
kind: "contains" ,
field: "address.city" ,
caseInsensitive: true ,
},
minLatitude: { kind: "gte" , field: "address.geo.lat" },
});
const employees : Employee [] = [
{
name: { first: "Alice" , last: "Chen" },
department: "engineering" ,
address: { city: "Portland" , geo: { lat: 45.5 , lng: - 122.7 } },
},
{
name: { first: "Bob" , last: "Smith" },
department: "design" ,
address: { city: "Seattle" , geo: { lat: 47.6 , lng: - 122.3 } },
},
];
// Filter by nested fields
const chenEmployees = employees . filter ( employeeFilter ({ lastName: "Chen" }));
const northernEmployees = employees . filter ( employeeFilter ({ minLatitude: 46 }));
The filter traverses each segment of the dot-separated path at runtime using a getByPath helper function.
Boolean Filters (AND/OR)
Combine multiple conditions with logical operators:
const productFilter = inMemoryFilter < Product >(). def ({
// OR: match any condition
searchTerm: {
kind: "or" ,
conditions: [
{ kind: "contains" , field: "name" },
{ kind: "contains" , field: "description" },
],
},
// AND: match all conditions
priceRange: {
kind: "and" ,
conditions: [
{ kind: "gte" , field: "price" },
{ kind: "lte" , field: "price" },
],
},
});
const results = products . filter (
productFilter ({
searchTerm: "laptop" , // Matches name OR description
priceRange: 500 , // Price >= 500 AND <= 500
})
);
All conditions in boolean filters must have explicit field properties.
Custom Filters
Define complex filtering logic with custom functions:
interface BlogPost {
id : string ;
title : string ;
content : string ;
author : string ;
tags : string [];
publishedAt : Date ;
viewCount : number ;
likeCount : number ;
commentCount : number ;
}
const postFilter = inMemoryFilter < BlogPost >(). def ({
// Standard primitive filters
id: { kind: "eq" },
author: { kind: "eq" },
titleContains: { kind: "contains" , field: "title" },
// Custom filter: Check if post has a specific tag
hasTag : ( post : BlogPost , tag : string ) => {
return post . tags . includes ( tag );
},
// Custom filter: Check if post has ALL provided tags
hasAllTags : ( post : BlogPost , tags : string []) => {
return tags . every (( tag ) => post . tags . includes ( tag ));
},
// Custom filter: Published within X days
publishedWithinDays : ( post : BlogPost , days : number ) => {
const cutoffDate = new Date ( Date . now () - days * 24 * 60 * 60 * 1000 );
return post . publishedAt >= cutoffDate ;
},
// Custom filter: Engagement rate calculation
minEngagementRate : ( post : BlogPost , minRate : number ) => {
const engagementRate = ( post . likeCount + post . commentCount ) / post . viewCount ;
return engagementRate >= minRate ;
},
// Custom filter: Popularity score (weighted calculation)
minPopularityScore : ( post : BlogPost , minScore : number ) => {
const score = post . viewCount * 1 + post . likeCount * 5 + post . commentCount * 10 ;
return score >= minScore ;
},
});
// Find trending posts
const trendingPosts = posts . filter (
postFilter ({
publishedWithinDays: 14 ,
minEngagementRate: 0.08 ,
minPopularityScore: 3000 ,
})
);
Nested Filters
Filter parent entities based on child array properties:
interface User {
name : string ;
posts : Post [];
}
interface Post {
id : string ;
title : string ;
}
const postFilter = inMemoryFilter < Post >(). def ({
id: { kind: "eq" },
titleContains: { kind: "contains" , field: "title" },
});
const userFilter = inMemoryFilter < User >(). def ({
name: { kind: "eq" },
// Custom filter using another filter
wrotePostWithId : ( user : User , postId : string ) =>
user . posts . some ( postFilter ({ id: postId })),
hasPostWithTitle : ( user : User , title : string ) =>
user . posts . some ( postFilter ({ titleContains: title })),
});
const authors = users . filter ( userFilter ({ hasPostWithTitle: "TypeScript" }));
Helper Functions
The makeFilterHelpers function creates convenience wrappers around array methods:
import { inMemoryFilter , makeFilterHelpers } from "@filter-def/in-memory" ;
const userFilter = inMemoryFilter < User >(). def ({
name: { kind: "eq" },
isActive: { kind: "eq" },
});
const {
filter : filterUsers ,
find : findUser ,
findIndex : findUserIndex ,
some : someUsers ,
every : everyUser ,
} = makeFilterHelpers ( userFilter );
// Use helper functions
const activeUsers = filterUsers ( users , { isActive: true });
const john = findUser ( users , { name: "John" });
const johnIndex = findUserIndex ( users , { name: "John" });
const hasActiveUsers = someUsers ( users , { isActive: true });
const allActive = everyUser ( users , { isActive: true });
Type Utilities
Use InMemoryFilterInput to extract the input type:
import type { InMemoryFilterInput } from "@filter-def/in-memory" ;
const userFilter = inMemoryFilter < User >(). def ({
name: { kind: "eq" },
minAge: { kind: "gte" , field: "age" },
});
type UserFilterInput = InMemoryFilterInput < typeof userFilter >;
// { name?: string; minAge?: number }
function searchUsers ( filters : UserFilterInput ) {
return users . filter ( userFilter ( filters ));
}
Custom Filter Type
Use InMemoryCustomFilter to type custom filter functions:
import type { InMemoryCustomFilter } from "@filter-def/in-memory" ;
type HasTagFilter = InMemoryCustomFilter < BlogPost , string >;
// (entity: BlogPost, input: string) => boolean
const hasTag : HasTagFilter = ( post , tag ) => post . tags . includes ( tag );
Complete Example
Here’s a real-world e-commerce search implementation:
import { inMemoryFilter } from "@filter-def/in-memory" ;
import type { InMemoryFilterInput } from "@filter-def/in-memory" ;
interface Product {
id : string ;
name : string ;
description : string ;
price : number ;
category : string ;
tags : string [];
inStock : boolean ;
rating : number ;
reviewCount : number ;
}
const productFilter = inMemoryFilter < Product >(). def ({
// Equality filters
id: { kind: "eq" },
category: { kind: "eq" },
inStock: { kind: "eq" },
// Range filters
minPrice: { kind: "gte" , field: "price" },
maxPrice: { kind: "lte" , field: "price" },
minRating: { kind: "gte" , field: "rating" },
// Array filters
categories: { kind: "inArray" , field: "category" },
// Text search (OR across multiple fields)
search: {
kind: "or" ,
conditions: [
{ kind: "contains" , field: "name" , caseInsensitive: true },
{ kind: "contains" , field: "description" , caseInsensitive: true },
],
},
// Custom filters
hasTag : ( product : Product , tag : string ) => product . tags . includes ( tag ),
minReviewCount : ( product : Product , count : number ) =>
product . reviewCount >= count ,
popularityScore : ( product : Product , minScore : number ) => {
const score = product . rating * product . reviewCount ;
return score >= minScore ;
},
});
type ProductFilterInput = InMemoryFilterInput < typeof productFilter >;
// Search function
function searchProducts (
products : Product [],
filters : ProductFilterInput
) : Product [] {
return products . filter ( productFilter ( filters ));
}
// Usage examples
const electronics = searchProducts ( products , {
category: "electronics" ,
inStock: true ,
maxPrice: 500 ,
});
const popularLaptops = searchProducts ( products , {
search: "laptop" ,
minRating: 4.0 ,
minReviewCount: 50 ,
inStock: true ,
});
const trendingProducts = searchProducts ( products , {
popularityScore: 100 ,
minPrice: 200 ,
maxPrice: 1000 ,
});
Features
Type-Safe Full TypeScript inference for filter inputs and entity fields
Composable Combine multiple filters with AND/OR logic
Native Integration Works with filter(), find(), some(), every()
Custom Logic Define complex business logic with custom functions
Nested Fields Filter on deeply nested properties with dot notation
Zero Dependencies Lightweight and framework-agnostic (only depends on @filter-def/core)
Limitations
Performance Considerations The in-memory adapter evaluates filters at runtime for each entity. For large datasets (10,000+ items), consider:
Indexing frequently filtered fields
Using pagination to limit result sets
Moving to a database adapter for server-side filtering
Null vs Undefined The isNull and isNotNull filters treat both null and undefined as null values. If you need to distinguish between them, use a custom filter.