Skip to main content

Overview

Boolean filters allow you to combine multiple filter conditions using AND and OR logic. They enable complex filtering scenarios like “search across multiple fields” or “match all criteria.”

OR Filters

An or filter passes when at least one of its conditions is met.
interface OrFilter<Entity> {
  kind: 'or';
  conditions: [PrimitiveFilter<Entity>, ...PrimitiveFilter<Entity>[]];
}

Basic OR Example

Search for users by name OR email:
import { inMemoryFilter } from '@filter-def/in-memory';

interface User {
  name: string;
  email: string;
  phone: string;
}

const userFilter = inMemoryFilter<User>().def({
  searchTerm: {
    kind: 'or',
    conditions: [
      { kind: 'contains', field: 'name' },
      { kind: 'contains', field: 'email' },
    ],
  },
});

const users: User[] = [
  { name: 'Alice Johnson', email: '[email protected]', phone: '555-0101' },
  { name: 'Bob Smith', email: '[email protected]', phone: '555-0102' },
];

// Matches users with "company" in name OR email
const results = users.filter(userFilter({ searchTerm: 'company' }));
// Returns: Alice (has "company" in email)

Multiple OR Conditions

OR filters can have more than two conditions:
const userFilter = inMemoryFilter<User>().def({
  contactInfo: {
    kind: 'or',
    conditions: [
      { kind: 'contains', field: 'name' },
      { kind: 'contains', field: 'email' },
      { kind: 'contains', field: 'phone' },
    ],
  },
});

// Matches if "555" appears in name, email, OR phone
const results = users.filter(userFilter({ contactInfo: '555' }));

OR with Different Filter Types

You can combine different primitive filter types in OR conditions:
interface User {
  email: string;
  phone: string | null;
  isVerified: boolean;
}

const userFilter = inMemoryFilter<User>().def({
  contactable: {
    kind: 'or',
    conditions: [
      { kind: 'contains', field: 'email' },      // Has company email
      { kind: 'isNotNull', field: 'phone' },     // OR has phone
    ],
  },
});

const reachable = users.filter(
  userFilter({ contactable: '@company.com' })
);

AND Filters

An and filter passes when all of its conditions are met.
interface AndFilter<Entity> {
  kind: 'and';
  conditions: [PrimitiveFilter<Entity>, ...PrimitiveFilter<Entity>[]];
}

Basic AND Example

Require multiple conditions to all be true:
interface User {
  name: string;
  email: string;
  isActive: boolean;
}

const userFilter = inMemoryFilter<User>().def({
  strictMatch: {
    kind: 'and',
    conditions: [
      { kind: 'eq', field: 'isActive' },
      { kind: 'contains', field: 'email' },
    ],
  },
});

// Must be active AND have company email
const strictResults = users.filter(
  userFilter({ strictMatch: true })
);
Note that when you pass true to the filter, it applies to the first condition (isActive). The second condition uses the same input value for the contains check.

AND for Complex Validation

interface Product {
  price: number;
  inStock: boolean;
  category: string;
}

const productFilter = inMemoryFilter<Product>().def({
  premiumAvailable: {
    kind: 'and',
    conditions: [
      { kind: 'gte', field: 'price' },     // Price >= threshold
      { kind: 'eq', field: 'inStock' },    // AND in stock
    ],
  },
});

const available = products.filter(
  productFilter({ premiumAvailable: 500 })
);
// Returns products that cost >= 500 AND are in stock

Combining with Other Filters

Boolean filters work seamlessly with primitive filters. Multiple filter keys are implicitly combined with AND logic:
const userFilter = inMemoryFilter<User>().def({
  role: { kind: 'eq' },
  isActive: { kind: 'eq' },
  
  // OR filter for search
  searchTerm: {
    kind: 'or',
    conditions: [
      { kind: 'contains', field: 'name' },
      { kind: 'contains', field: 'email' },
    ],
  },
});

// ALL of these must be true:
// - role = 'admin'
// - isActive = true  
// - (name contains 'alice' OR email contains 'alice')
const results = users.filter(userFilter({
  role: 'admin',
  isActive: true,
  searchTerm: 'alice',
}));

Required Field Properties

All conditions in boolean filters (AND/OR) must have explicit field properties. This is validated at compile time.
// ❌ Invalid: conditions missing field properties
const invalidFilter = inMemoryFilter<User>().def({
  search: {
    kind: 'or',
    conditions: [
      { kind: 'contains' },  // Error: missing field property
      { kind: 'contains' },  // Error: missing field property
    ],
  },
});

// ✅ Valid: all conditions have explicit fields
const validFilter = inMemoryFilter<User>().def({
  search: {
    kind: 'or',
    conditions: [
      { kind: 'contains', field: 'name' },
      { kind: 'contains', field: 'email' },
    ],
  },
});
This requirement ensures type safety and prevents ambiguity in boolean filter conditions.

Real-World Examples

const blogFilter = inMemoryFilter<BlogPost>().def({
  query: {
    kind: 'or',
    conditions: [
      { kind: 'contains', field: 'title' },
      { kind: 'contains', field: 'content' },
      { kind: 'contains', field: 'author' },
    ],
  },
  isPublished: { kind: 'eq' },
});

const searchResults = posts.filter(blogFilter({
  query: 'typescript',
  isPublished: true,
}));
// Returns published posts with "typescript" in title, content, or author

Flexible Contact Filtering

interface Contact {
  email: string;
  phone: string | null;
  preferredContact: 'email' | 'phone';
}

const contactFilter = inMemoryFilter<Contact>().def({
  hasEmailOrPhone: {
    kind: 'or',
    conditions: [
      { kind: 'contains', field: 'email' },
      { kind: 'isNotNull', field: 'phone' },
    ],
  },
  preferredContact: { kind: 'eq' },
});

const emailReachable = contacts.filter(contactFilter({
  hasEmailOrPhone: '@company.com',
  preferredContact: 'email',
}));

Role-Based Access

interface User {
  role: string;
  permissions: string[];
  isActive: boolean;
}

const userFilter = inMemoryFilter<User>().def({
  canAccess: {
    kind: 'or',
    conditions: [
      { kind: 'eq', field: 'role' },           // Is admin
      { kind: 'contains', field: 'permissions' }, // OR has permission
    ],
  },
  isActive: { kind: 'eq' },
});

const authorized = users.filter(userFilter({
  canAccess: 'admin',  // admin role OR has 'admin' in permissions
  isActive: true,
}));

Boolean Filter Behavior

Empty Conditions

Boolean filters require at least one condition (enforced by TypeScript):
// ❌ Compile error: conditions array must have at least 1 element
const invalid = inMemoryFilter<User>().def({
  search: {
    kind: 'or',
    conditions: [],  // Error
  },
});

Single Condition

Boolean filters with a single condition are valid but redundant:
// Valid but unnecessary - just use the condition directly
const userFilter = inMemoryFilter<User>().def({
  emailSearch: {
    kind: 'or',
    conditions: [
      { kind: 'contains', field: 'email' },
    ],
  },
});

// Better: use primitive filter directly
const betterFilter = inMemoryFilter<User>().def({
  emailSearch: { kind: 'contains', field: 'email' },
});

Type Definitions

import type { AndFilter, OrFilter, BooleanFilter } from '@filter-def/core';

// Union of AND and OR filters
type BooleanFilter<Entity> = AndFilter<Entity> | OrFilter<Entity>;

// AND filter
interface AndFilter<Entity> {
  kind: 'and';
  conditions: [PrimitiveFilter<Entity>, ...PrimitiveFilter<Entity>[]];
}

// OR filter
interface OrFilter<Entity> {
  kind: 'or';
  conditions: [PrimitiveFilter<Entity>, ...PrimitiveFilter<Entity>[]];
}

Limitations

Boolean filters cannot be nested. You cannot have an OR filter inside an AND filter, or vice versa.
// ❌ Not supported: nested boolean filters
const invalid = inMemoryFilter<User>().def({
  complex: {
    kind: 'and',
    conditions: [
      {
        kind: 'or',  // Cannot nest boolean filters
        conditions: [/* ... */],
      },
    ],
  },
});
For complex nested logic, use Custom Filters instead.

Next Steps

Build docs developers (and LLMs) love