Skip to main content

Overview

Filter definitions are the core of filter-def. They specify what fields can be filtered and how. Once defined, a filter can be reused across your application with full TypeScript type safety.

The .def() Method

Every adapter provides a .def() method to create filter definitions. The method accepts a filter definition object where each key represents a filterable input parameter.
import { inMemoryFilter } from '@filter-def/in-memory';

interface User {
  id: string;
  name: string;
  email: string;
  age: number;
}

const userFilter = inMemoryFilter<User>().def({
  name: { kind: 'eq' },
  emailContains: { kind: 'contains', field: 'email' },
  minAge: { kind: 'gte', field: 'age' },
});

// Use the filter
const users: User[] = [/* ... */];
const filtered = users.filter(userFilter({ name: 'John', minAge: 18 }));

Filter Definition Structure

Each filter definition is an object where:
  • Key: The input parameter name (e.g., emailContains, minAge)
  • Value: A filter specification object with a kind property
const filterDef = {
  // Filter key: filter specification
  name: { kind: 'eq' },                      // Simple equality
  minAge: { kind: 'gte', field: 'age' },     // Greater than or equal
  emailContains: { kind: 'contains', field: 'email' },  // String search
};

Field Inference

When a filter key matches an entity field name, the field property is optional and will be automatically inferred:
interface Product {
  id: string;
  name: string;
  price: number;
  category: string;
}

const productFilter = inMemoryFilter<Product>().def({
  // ✅ Field inferred from key
  name: { kind: 'eq' },          // field: 'name' is automatic
  category: { kind: 'eq' },      // field: 'category' is automatic
  price: { kind: 'gte' },        // field: 'price' is automatic
  
  // ✅ Explicit field when key differs
  nameContains: { kind: 'contains', field: 'name' },
  minPrice: { kind: 'gte', field: 'price' },
  maxPrice: { kind: 'lte', field: 'price' },
});
Field inference makes definitions more concise while maintaining type safety. TypeScript will validate that inferred field names exist on your entity type.

Multiple Filters on Same Field

You can create multiple filters targeting the same entity field with different filter keys:
const productFilter = inMemoryFilter<Product>().def({
  minPrice: { kind: 'gte', field: 'price' },
  maxPrice: { kind: 'lte', field: 'price' },
  exactPrice: { kind: 'eq', field: 'price' },
});

// Create a price range filter
const filtered = products.filter(
  productFilter({ minPrice: 100, maxPrice: 500 })
);
This pattern is useful for:
  • Price ranges (minPrice, maxPrice)
  • Date ranges (startDate, endDate)
  • Numeric boundaries (minAge, maxAge)

Using Filter Definitions

Once defined, filters are called with an input object. All filter parameters are optional:
const userFilter = inMemoryFilter<User>().def({
  name: { kind: 'eq' },
  emailContains: { kind: 'contains', field: 'email' },
  minAge: { kind: 'gte', field: 'age' },
});

// All parameters optional
userFilter()                                    // No filtering
userFilter({})                                  // No filtering
userFilter({ name: 'John' })                    // Filter by name only
userFilter({ name: 'John', minAge: 18 })        // Multiple filters (AND logic)
When no filter values are provided or all values are undefined, the filter passes all entities/rows.

Type Safety

Filter definitions provide complete TypeScript type safety:
const productFilter = inMemoryFilter<Product>().def({
  name: { kind: 'eq' },
  minPrice: { kind: 'gte', field: 'price' },
});

// ✅ Valid
productFilter({ name: 'Laptop' });
productFilter({ minPrice: 100 });
productFilter({ name: 'Laptop', minPrice: 100 });

// ❌ TypeScript errors
productFilter({ name: 123 });           // Error: name must be string
productFilter({ minPrice: 'invalid' }); // Error: minPrice must be number
productFilter({ invalid: true });       // Error: 'invalid' not in filter def

Validation at Definition Time

The ValidateFilterDef type ensures filters are correctly defined:
// ❌ Compile-time error: 'firstName' is not a field on User
const invalidFilter = inMemoryFilter<User>().def({
  firstName: { kind: 'eq' },  // Error: must specify field or match entity field
});

// ✅ Fixed with explicit field
const validFilter = inMemoryFilter<User>().def({
  firstName: { kind: 'eq', field: 'name' },
});
For boolean filters (AND/OR), all conditions must have explicit field properties. See Boolean Filters for details.

Reusable Filters

Filter definitions are highly reusable. Define once, use everywhere:
// Define in a shared module
export const productFilter = inMemoryFilter<Product>().def({
  name: { kind: 'eq' },
  category: { kind: 'eq' },
  minPrice: { kind: 'gte', field: 'price' },
  maxPrice: { kind: 'lte', field: 'price' },
  inStock: { kind: 'eq' },
});

// Use throughout your application
// products.ts
const inStockProducts = products.filter(productFilter({ inStock: true }));

// api-handler.ts
app.get('/products', (req) => {
  const filtered = products.filter(productFilter(req.query));
  res.json(filtered);
});

// components/ProductList.tsx
const filteredProducts = products.filter(
  productFilter({ category: selectedCategory, inStock: true })
);

Cross-Adapter Compatibility

Filter definitions use the same syntax across all adapters. You can switch adapters with minimal code changes:
// Same filter definition structure
const filterDef = {
  name: { kind: 'eq' },
  minAge: { kind: 'gte', field: 'age' },
};

// In-memory adapter
const memoryFilter = inMemoryFilter<User>().def(filterDef);

// Drizzle adapter (same definition!)
const drizzleFilter = drizzleFilter(usersTable).def(filterDef);

// BigQuery adapter (same definition!)
const bqFilter = bigqueryFilter<User>().def(filterDef);

Next Steps

Build docs developers (and LLMs) love