Skip to main content

Type Safety

Always Extract Input Types

Define explicit types for filter inputs to improve code maintainability and catch errors at compile time:
import { inMemoryFilter } from "@filter-def/in-memory";
import type { InMemoryFilterInput } from "@filter-def/in-memory";

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

// ✅ Extract the input type
type UserFilterInput = InMemoryFilterInput<typeof userFilter>;

// Use it in function signatures
function fetchUsers(filters: UserFilterInput): User[] {
    return users.filter(userFilter(filters));
}
Extracting input types provides better IDE autocomplete and prevents invalid filter values.

Use Explicit Field Properties

When filter names don’t match entity fields, always use explicit field properties:
// ❌ Avoid: Confusing filter name without explicit field
const userFilter = inMemoryFilter<User>().def({
    q: { kind: "contains" }, // What field does this filter?
});

// ✅ Good: Explicit field makes intent clear
const userFilter = inMemoryFilter<User>().def({
    searchQuery: { kind: "contains", field: "name" },
});

Validate Filter Definitions at Build Time

Take advantage of TypeScript’s validation to catch errors early:
interface User {
    name: string;
    email: string;
    age: number;
}

// ❌ This will cause a type error (good!)
const userFilter = inMemoryFilter<User>().def({
    firstName: { kind: "eq" }, // ❌ 'firstName' is not a field of User
});

// ✅ Valid filter definitions
const userFilter = inMemoryFilter<User>().def({
    name: { kind: "eq" }, // ✅ 'name' matches User field
    firstName: { kind: "eq", field: "name" }, // ✅ Explicit field provided
});

Leverage Type Inference

Let TypeScript infer types when the filter name matches the field name:
const userFilter = inMemoryFilter<User>().def({
    // Field name matches filter name - field property is optional
    name: { kind: "eq" },
    email: { kind: "contains" },
    age: { kind: "gte" },
    
    // Field name differs - field property required
    minAge: { kind: "gte", field: "age" },
    searchTerm: { kind: "contains", field: "name" },
});

Performance

Pre-Compile Filters

Define filters once outside of render loops or request handlers:
// Define filter at module level
const userFilter = inMemoryFilter<User>().def({
    name: { kind: "eq" },
    minAge: { kind: "gte", field: "age" },
});

function searchUsers(filters: UserFilterInput) {
    // Reuse pre-compiled filter
    return users.filter(userFilter(filters));
}
Defining filters inside functions causes unnecessary recompilation on every invocation.

Avoid Unnecessary Filter Fields

Only define filters you actually use:
// ❌ Defining too many unused filters
const userFilter = inMemoryFilter<User>().def({
    name: { kind: "eq" },
    email: { kind: "eq" },
    age: { kind: "eq" },
    phone: { kind: "eq" },
    address: { kind: "eq" },
    // ... many more fields you never use
});

// ✅ Define only what you need
const userFilter = inMemoryFilter<User>().def({
    name: { kind: "eq" },
    minAge: { kind: "gte", field: "age" },
});

Optimize Boolean Filters

When using or filters with many conditions, consider whether a different approach might be more efficient:
// For searching across multiple fields
const userFilter = inMemoryFilter<User>().def({
    searchTerm: {
        kind: "or",
        conditions: [
            { kind: "contains", field: "name" },
            { kind: "contains", field: "email" },
            { kind: "contains", field: "phone" },
        ],
    },
});

// Or use a custom filter for complex logic
const userFilter = inMemoryFilter<User>().def({
    searchTerm: (user, term: string) => {
        const lowerTerm = term.toLowerCase();
        return (
            user.name.toLowerCase().includes(lowerTerm) ||
            user.email.toLowerCase().includes(lowerTerm) ||
            user.phone?.toLowerCase().includes(lowerTerm)
        );
    },
});

Use Appropriate Adapters

Choose the right adapter for your data source:

In-Memory

Use for:
  • Small datasets (< 10,000 items)
  • Client-side filtering
  • Already-loaded data
Avoid for:
  • Large datasets requiring pagination
  • Data that should be filtered at the database level

SQL (Drizzle)

Use for:
  • Large datasets
  • Server-side filtering
  • When you need database indexes
Avoid for:
  • In-memory arrays
  • Client-side filtering

Error Handling

Handle Empty Filter Inputs

All adapters handle empty filter inputs gracefully:
const userFilter = inMemoryFilter<User>().def({
    name: { kind: "eq" },
    minAge: { kind: "gte", field: "age" },
});

// These all return truthy results (no filtering)
users.filter(userFilter()); // undefined input
users.filter(userFilter({})); // empty object

// Only specified filters are applied
users.filter(userFilter({ name: "John" })); // Only name filter

Validate User Input

When accepting filter values from user input, validate them first:
import { z } from "zod";

const UserFilterInputSchema = z.object({
    name: z.string().optional(),
    minAge: z.number().min(0).max(150).optional(),
    emailContains: z.string().email().optional(),
});

function searchUsers(rawInput: unknown) {
    // Validate user input
    const validatedInput = UserFilterInputSchema.parse(rawInput);
    
    // Safe to use validated input
    return users.filter(userFilter(validatedInput));
}

Handle Missing Fields Gracefully

When working with optional fields, ensure your filters handle undefined values:
interface User {
    name: string;
    email: string;
    phone?: string; // Optional field
}

const userFilter = inMemoryFilter<User>().def({
    phoneContains: { kind: "contains", field: "phone" },
});

// The filter will safely handle undefined phone values
const results = users.filter(
    userFilter({ phoneContains: "555" })
);
filter-def automatically handles undefined/null field values in all adapters.

Code Organization

Centralize Filter Definitions

Define all filters for an entity in one place:
filters/user-filters.ts
import { inMemoryFilter } from "@filter-def/in-memory";
import type { InMemoryFilterInput } from "@filter-def/in-memory";
import type { User } from "@/types/user";

export const userFilter = inMemoryFilter<User>().def({
    name: { kind: "eq" },
    nameContains: { kind: "contains", field: "name" },
    email: { kind: "eq" },
    emailContains: { kind: "contains", field: "email" },
    minAge: { kind: "gte", field: "age" },
    maxAge: { kind: "lte", field: "age" },
    isActive: { kind: "eq" },
});

export type UserFilterInput = InMemoryFilterInput<typeof userFilter>;
Then import and use throughout your application:
services/user-service.ts
import { userFilter } from "@/filters/user-filters";
import type { UserFilterInput } from "@/filters/user-filters";

export function findUsers(filters: UserFilterInput): User[] {
    return users.filter(userFilter(filters));
}

Create Filter Presets

Define common filter combinations as presets:
filters/user-filters.ts
import type { UserFilterInput } from "./user-filters";

export const USER_FILTER_PRESETS = {
    activeAdults: {
        isActive: true,
        minAge: 18,
    } satisfies UserFilterInput,
    
    premiumUsers: {
        subscriptionType: "premium",
        isActive: true,
    } satisfies UserFilterInput,
    
    recentSignups: {
        minCreatedAt: Date.now() - 7 * 24 * 60 * 60 * 1000, // Last 7 days
    } satisfies UserFilterInput,
};

// Usage
const activeAdults = users.filter(
    userFilter(USER_FILTER_PRESETS.activeAdults)
);

Compose Filters Dynamically

Build filter inputs conditionally based on application logic:
import type { UserFilterInput } from "@/filters/user-filters";

function buildSearchFilters(
    searchTerm?: string,
    ageRange?: { min?: number; max?: number },
    activeOnly?: boolean
): UserFilterInput {
    const filters: UserFilterInput = {};
    
    if (searchTerm) {
        filters.nameContains = searchTerm;
    }
    
    if (ageRange?.min !== undefined) {
        filters.minAge = ageRange.min;
    }
    
    if (ageRange?.max !== undefined) {
        filters.maxAge = ageRange.max;
    }
    
    if (activeOnly) {
        filters.isActive = true;
    }
    
    return filters;
}

// Usage
const filters = buildSearchFilters("John", { min: 18, max: 65 }, true);
const results = users.filter(userFilter(filters));

Testing

Test Filter Definitions

Write tests for your filter definitions:
user-filters.spec.ts
import { describe, expect, it } from "vitest";
import { userFilter } from "./user-filters";

const mockUsers: User[] = [
    { id: 1, name: "John Doe", email: "[email protected]", age: 30 },
    { id: 2, name: "Jane Doe", email: "[email protected]", age: 25 },
    { id: 3, name: "Bob Smith", email: "[email protected]", age: 40 },
];

describe("userFilter", () => {
    it("should filter by name", () => {
        const results = mockUsers.filter(
            userFilter({ name: "John Doe" })
        );
        
        expect(results).toHaveLength(1);
        expect(results[0].name).toBe("John Doe");
    });
    
    it("should filter by age range", () => {
        const results = mockUsers.filter(
            userFilter({ minAge: 25, maxAge: 35 })
        );
        
        expect(results).toHaveLength(2);
        expect(results.map(u => u.name)).toEqual(["John Doe", "Jane Doe"]);
    });
    
    it("should combine multiple filters", () => {
        const results = mockUsers.filter(
            userFilter({
                emailContains: "example",
                minAge: 30,
            })
        );
        
        expect(results).toHaveLength(2);
    });
});

Mock Filter Inputs

Create test fixtures for filter inputs:
__fixtures__/user-filters.ts
import type { UserFilterInput } from "@/filters/user-filters";

export const TEST_USER_FILTERS = {
    johnDoe: {
        name: "John Doe",
    } satisfies UserFilterInput,
    
    adults: {
        minAge: 18,
    } satisfies UserFilterInput,
    
    exampleEmails: {
        emailContains: "example.com",
    } satisfies UserFilterInput,
};

Test Type Safety

Use expectTypeOf to test type inference:
user-filters.spec.ts
import { expectTypeOf } from "vitest";
import { userFilter } from "./user-filters";
import type { UserFilterInput } from "./user-filters";

it("should infer correct input type", () => {
    expectTypeOf<UserFilterInput>().toMatchTypeOf<{
        name?: string;
        nameContains?: string;
        email?: string;
        minAge?: number;
        maxAge?: number;
        isActive?: boolean;
    }>();
});

Debugging

Log Filter Compilation

When debugging, log the compiled filter result:
const predicate = userFilter({ name: "John" });
console.log("Filter predicate:", predicate.toString());

const results = users.filter(predicate);
console.log("Filtered results:", results);

Verify Filter Logic

Test filters with known data to verify behavior:
const testData = [
    { id: 1, name: "John", age: 30 },
    { id: 2, name: "Jane", age: 25 },
];

const filter = userFilter({ minAge: 26 });
const results = testData.filter(filter);

console.log("Expected: John (30)");
console.log("Actual:", results);

Security

Sanitize User Input

Always validate and sanitize filter values from user input:
import { z } from "zod";

// Define strict schema for user-provided filters
const SearchFiltersSchema = z.object({
    name: z.string().max(100).optional(),
    minAge: z.number().int().min(0).max(150).optional(),
    email: z.string().email().max(255).optional(),
});

function searchUsers(untrustedInput: unknown) {
    try {
        const validFilters = SearchFiltersSchema.parse(untrustedInput);
        return users.filter(userFilter(validFilters));
    } catch (error) {
        console.error("Invalid filter input:", error);
        return [];
    }
}

Prevent SQL Injection (Drizzle/BigQuery)

All official adapters use parameterized queries, but be careful with custom filters:
const userFilter = drizzleFilter(usersTable).def({
    // ✅ Safe: Uses Drizzle's parameterized queries
    name: { kind: "eq" },
    
    // ✅ Safe: Custom filter with proper parameterization
    customSearch: (term: string) => sql`name ILIKE ${'%' + term + '%'}`,
});

Limit Filter Complexity

Prevent resource exhaustion by limiting filter complexity:
function validateFilterComplexity(filters: UserFilterInput): boolean {
    const filterCount = Object.keys(filters).length;
    
    if (filterCount > 10) {
        throw new Error("Too many filters specified (max: 10)");
    }
    
    return true;
}

function searchUsers(filters: UserFilterInput) {
    validateFilterComplexity(filters);
    return users.filter(userFilter(filters));
}

Next Steps

API Reference

Explore the complete API documentation

Core Concepts

Learn about filter definitions and types

Building Adapters

Learn how to create custom adapters

Migration Guide

Upgrade from v1 to v2

Build docs developers (and LLMs) love