Type System
Understanding the TypeScript type system in @mcabreradev/filter.
Overview
@mcabreradev/filter is built with TypeScript-first design, providing full type safety and intelligent autocomplete through advanced type inference. The type system adapts to your data structure, suggesting only valid operators for each property type.
Core Type Definitions
Expression<T>
The main type for filter expressions that adapts to your data structure with intelligent operator suggestions.
type Expression<T> =
| PrimitiveExpression
| PredicateFunction<T>
| ObjectExpression<T>
| LogicalExpression<T>;
// Object expression with typed operators
type ObjectExpression<T> = {
[K in keyof T]?:
| T[K]
| OperatorExpression<T[K]>
| T[K][]; // Array OR syntax (v5.5.0+)
} & {
$and?: Expression<T>[];
$or?: Expression<T>[];
$not?: Expression<T>;
};
// Primitive expressions
type PrimitiveExpression = string | number | boolean | null;
// Predicate function
type PredicateFunction<T> = (item: T) => boolean;
// Logical expressions
type LogicalExpression<T> = {
$and?: Expression<T>[];
$or?: Expression<T>[];
$not?: Expression<T>;
};How it works:
- Maps over all properties of type
T - Each property can have an operator expression, direct value, or array of values (OR logic)
- Supports logical expressions (
$and,$or,$not) - TypeScript suggests only valid operators for each property type
Example:
interface User {
id: number;
name: string;
age: number;
active: boolean;
tags: string[];
location: GeoPoint;
birthDate: Date;
}
const expression: Expression<User> = {
// Number property - suggests: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin
age: { $gte: 18, $lte: 65 },
// String property - suggests: $eq, $ne, $startsWith, $endsWith, $contains, $regex, $match, $in, $nin
name: { $startsWith: 'John' },
// Array property - suggests: $contains, $size, $in, $nin
tags: { $contains: 'premium' },
// GeoPoint property - suggests: $near, $geoBox, $geoPolygon
location: {
$near: {
center: { lat: 52.52, lng: 13.405 },
maxDistanceMeters: 5000
}
},
// Date property - suggests: $recent, $upcoming, $dayOfWeek, $timeOfDay, $age, etc.
birthDate: { $age: { min: 18, max: 65, unit: 'years' } },
// Boolean property - suggests: $eq, $ne
active: { $eq: true },
// Array OR syntax (v5.5.0+)
id: [1, 2, 3] // Matches id === 1 OR id === 2 OR id === 3
};OperatorExpression<T>
Type-safe operator expressions that adapt to property types with intelligent autocomplete.
type OperatorExpression<T> =
& ComparisonOperators<T>
& ArrayOperators<T>
& StringOperators<T>
& GeospatialOperators<T>
& DateTimeOperators<T>;
// Comparison operators (available for all types)
interface ComparisonOperators<T> {
$eq?: T;
$ne?: T;
$gt?: T extends number | Date ? T : never;
$gte?: T extends number | Date ? T : never;
$lt?: T extends number | Date ? T : never;
$lte?: T extends number | Date ? T : never;
}
// Array operators (available for array types)
interface ArrayOperators<T> {
$in?: T[];
$nin?: T[];
$contains?: T extends Array<infer U> ? U : never;
$size?: T extends unknown[] ? number : never;
}
// String operators (available for string types)
interface StringOperators<T> {
$startsWith?: T extends string ? string : never;
$endsWith?: T extends string ? string : never;
$contains?: T extends string ? string : never;
$regex?: T extends string ? RegExp | string : never;
$match?: T extends string ? RegExp | string : never;
}
// Geospatial operators (v5.6.0+, available for GeoPoint types)
interface GeospatialOperators<T> {
$near?: T extends GeoPoint ? NearQuery : never;
$geoBox?: T extends GeoPoint ? BoundingBox : never;
$geoPolygon?: T extends GeoPoint ? PolygonQuery : never;
}
// Datetime operators (v5.6.0+, available for Date types)
interface DateTimeOperators<T> {
$recent?: T extends Date ? RelativeTimeQuery : never;
$upcoming?: T extends Date ? RelativeTimeQuery : never;
$dayOfWeek?: T extends Date ? number[] : never;
$timeOfDay?: T extends Date ? TimeOfDayQuery : never;
$age?: T extends Date ? AgeQuery : never;
$isWeekday?: T extends Date ? boolean : never;
$isWeekend?: T extends Date ? boolean : never;
$isBefore?: T extends Date ? Date : never;
$isAfter?: T extends Date ? Date : never;
}Type Constraints:
- Comparison operators (
$gt,$gte,$lt,$lte) only work withnumberorDate - String operators (
$regex,$startsWith,$endsWith,$contains,$match) only work withstring - Array operators (
$contains,$size) only work with arrays - Geospatial operators (
$near,$geoBox,$geoPolygon) only work withGeoPoint - Datetime operators (
$recent,$upcoming,$dayOfWeek, etc.) only work withDate
Example:
interface Product {
price: number;
name: string;
tags: string[];
location: GeoPoint;
createdAt: Date;
}
const expression: Expression<Product> = {
// TypeScript suggests only valid operators for each property
price: { $gte: 100, $lte: 500 }, // number: $gt, $gte, $lt, $lte, $eq, $ne, $in, $nin
name: { $startsWith: 'Pro' }, // string: $startsWith, $endsWith, $contains, $regex, $match, $eq, $ne, $in, $nin
tags: { $contains: 'sale' }, // array: $contains, $size, $in, $nin
location: { // GeoPoint: $near, $geoBox, $geoPolygon
$near: {
center: { lat: 40.7128, lng: -74.0060 },
maxDistanceMeters: 10000
}
},
createdAt: { // Date: $recent, $upcoming, $dayOfWeek, $timeOfDay, $age, etc.
$recent: { days: 30 }
}
};Geospatial Types (v5.6.0+)
GeoPoint
Represents a geographic coordinate point.
export interface GeoPoint {
lat: number; // Latitude: -90 to 90
lng: number; // Longitude: -180 to 180
}NearQuery
Query for finding points within a radius.
export interface NearQuery {
center: GeoPoint;
maxDistanceMeters: number; // Maximum distance in meters
}Example:
interface Restaurant {
name: string;
location: GeoPoint;
}
const expression: Expression<Restaurant> = {
location: {
$near: {
center: { lat: 40.7128, lng: -74.0060 },
maxDistanceMeters: 5000 // 5km radius
}
}
};BoundingBox
Defines a rectangular geographic area.
export interface BoundingBox {
southwest: GeoPoint; // Southwest corner
northeast: GeoPoint; // Northeast corner
}Example:
const expression: Expression<Restaurant> = {
location: {
$geoBox: {
southwest: { lat: 40.7, lng: -74.1 },
northeast: { lat: 40.8, lng: -73.9 }
}
}
};PolygonQuery
Defines a polygon for containment queries.
export interface PolygonQuery {
points: GeoPoint[]; // Array of points defining the polygon
}Example:
const expression: Expression<Restaurant> = {
location: {
$geoPolygon: {
points: [
{ lat: 40.7128, lng: -74.0060 },
{ lat: 40.7580, lng: -73.9855 },
{ lat: 40.7489, lng: -73.9680 },
{ lat: 40.7128, lng: -74.0060 } // Close the polygon
]
}
}
};Datetime Types (v5.6.0+)
RelativeTimeQuery
Query for relative time ranges (last/next N days/hours/minutes).
export interface RelativeTimeQuery {
days?: number; // Number of days
hours?: number; // Number of hours
minutes?: number; // Number of minutes
}Example:
interface Event {
name: string;
date: Date;
}
const expression: Expression<Event> = {
date: {
$recent: { days: 7 } // Events in last 7 days
}
};
const upcoming: Expression<Event> = {
date: {
$upcoming: { hours: 24 } // Events in next 24 hours
}
};TimeOfDayQuery
Query for filtering by time of day.
export interface TimeOfDayQuery {
start: number; // Start hour (0-23)
end: number; // End hour (0-23)
}Example:
const expression: Expression<Event> = {
date: {
$timeOfDay: {
start: 9, // 9 AM
end: 17 // 5 PM
}
}
};AgeQuery
Query for calculating and filtering by age.
export interface AgeQuery {
min?: number; // Minimum age
max?: number; // Maximum age
unit?: 'years' | 'months' | 'days'; // Age unit (default: 'years')
}Example:
interface User {
name: string;
birthDate: Date;
}
const expression: Expression<User> = {
birthDate: {
$age: {
min: 18,
max: 65,
unit: 'years'
}
}
};Debug Types (v5.5.0+)
DebugResult
Result from debug-enabled filtering.
export interface DebugResult<T> {
items: T[]; // Filtered items
stats: DebugStats; // Performance and match statistics
debug: {
tree: DebugNode; // Expression tree
expression: Expression<T>; // Original expression
};
print: () => void; // Print debug tree to console
}DebugStats
Statistics about filter execution.
export interface DebugStats {
matched: number; // Number of matched items
total: number; // Total number of items
percentage: number; // Match percentage
executionTime: number; // Execution time in milliseconds
conditionsEvaluated: number; // Number of conditions evaluated
}DebugNode
Node in the debug expression tree.
export interface DebugNode {
type: 'logical' | 'operator' | 'property' | 'primitive';
label: string;
operator?: string;
matched?: number;
total?: number;
children?: DebugNode[];
}Example:
import { filterDebug } from '@mcabreradev/filter';
const result = filterDebug(users, { age: { $gte: 18 } });
console.log(result.stats);
// {
// matched: 150,
// total: 200,
// percentage: 75,
// executionTime: 1.23,
// conditionsEvaluated: 200
// }
result.print(); // Print colorized debug treeConfiguration Types
FilterOptions
Configuration options for filtering.
export interface FilterOptions {
caseSensitive?: boolean; // Case-sensitive string matching (default: false)
maxDepth?: number; // Max depth for nested objects (default: 3, range: 1-10)
enableCache?: boolean; // Enable result caching (default: false)
debug?: boolean; // Enable debug mode (default: false)
verbose?: boolean; // Show detailed debug info (default: false)
showTimings?: boolean; // Show execution timings (default: false)
colorize?: boolean; // Use ANSI colors in debug output (default: false)
customComparator?: Comparator; // Custom comparison function
}FilterConfig
Internal configuration after merging with defaults.
export interface FilterConfig {
caseSensitive: boolean;
maxDepth: number;
enableCache: boolean;
debug: boolean;
verbose: boolean;
showTimings: boolean;
colorize: boolean;
customComparator?: Comparator;
}Comparator
Custom comparison function type.
export type Comparator = (actual: unknown, expected: unknown) => boolean;Example:
const customComparator: Comparator = (actual, expected) => {
// Custom fuzzy string matching
if (typeof actual === 'string' && typeof expected === 'string') {
return actual.toLowerCase().includes(expected.toLowerCase());
}
return actual === expected;
};
const filtered = filter(users, 'john', { customComparator });Advanced Type Features
Nested Object Support
The type system supports nested object filtering with dot notation.
interface User {
profile: {
address: {
city: string;
country: string;
};
};
}
const expression: Expression<User> = {
'profile.address.city': { $eq: 'New York' }
};
const expression2: Expression<User> = {
profile: {
address: {
city: { $eq: 'New York' }
}
}
};Type Inference
The library automatically infers types from your data:
const users = [
{ id: 1, name: 'John', age: 25 },
{ id: 2, name: 'Jane', age: 30 }
];
const { filtered } = useFilter(users, {
age: { $gte: 18 }
});Generic Constraints
Use generic constraints for reusable filter functions:
function createAgeFilter<T extends { age: number }>(minAge: number): Expression<T> {
return {
age: { $gte: minAge }
};
}
interface User {
id: number;
name: string;
age: number;
}
interface Employee {
employeeId: string;
age: number;
department: string;
}
const userFilter = createAgeFilter<User>(18);
const employeeFilter = createAgeFilter<Employee>(21);Framework-Specific Types
React Types
interface UseFilterResult<T> {
filtered: T[];
isFiltering: boolean;
}
interface UseFilteredStateResult<T> {
data: T[];
setData: (data: T[]) => void;
expression: Expression<T>;
setExpression: (expression: Expression<T>) => void;
filtered: T[];
isFiltering: boolean;
}
interface UseDebouncedFilterResult<T> {
filtered: T[];
isFiltering: boolean;
isPending: boolean;
}
interface UseDebouncedFilterOptions extends FilterOptions {
delay?: number; // Debounce delay in milliseconds (default: 300)
}
interface UsePaginatedFilterResult<T> {
filtered: T[];
isFiltering: boolean;
currentPage: number;
totalPages: number;
pageSize: number;
totalItems: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
nextPage: () => void;
previousPage: () => void;
goToPage: (page: number) => void;
setPageSize: (size: number) => void;
}
interface UsePaginatedFilterOptions extends FilterOptions {
initialPage?: number; // Initial page number (default: 1)
initialPageSize?: number; // Initial page size (default: 10)
}Example:
import { useFilter, useDebouncedFilter, usePaginatedFilter } from '@mcabreradev/filter/react';
function UserList() {
// Basic filtering
const { filtered, isFiltering } = useFilter(users, { active: true });
// Debounced filtering for search
const [search, setSearch] = useState('');
const { filtered: searchResults, isPending } = useDebouncedFilter(
users,
{ name: { $contains: search } },
{ delay: 300 }
);
// Paginated filtering
const {
filtered: paginatedUsers,
currentPage,
totalPages,
nextPage,
previousPage
} = usePaginatedFilter(
users,
{ active: true },
{ initialPage: 1, initialPageSize: 20 }
);
return <div>{/* ... */}</div>;
}Vue Types
import type { Ref, ComputedRef } from 'vue';
type MaybeRef<T> = T | Ref<T> | ComputedRef<T>;
interface UseFilterResult<T> {
filtered: ComputedRef<T[]>;
isFiltering: ComputedRef<boolean>;
}
interface UseFilteredStateResult<T> {
data: Ref<T[]>;
expression: Ref<Expression<T>>;
filtered: ComputedRef<T[]>;
isFiltering: ComputedRef<boolean>;
}
interface UseDebouncedFilterResult<T> {
filtered: ComputedRef<T[]>;
isFiltering: ComputedRef<boolean>;
isPending: Ref<boolean>;
}
interface UsePaginatedFilterResult<T> {
filtered: ComputedRef<T[]>;
isFiltering: ComputedRef<boolean>;
currentPage: Ref<number>;
totalPages: ComputedRef<number>;
pageSize: Ref<number>;
totalItems: ComputedRef<number>;
hasNextPage: ComputedRef<boolean>;
hasPreviousPage: ComputedRef<boolean>;
nextPage: () => void;
previousPage: () => void;
goToPage: (page: number) => void;
setPageSize: (size: number) => void;
}Example:
import { ref } from 'vue';
import { useFilter, useDebouncedFilter } from '@mcabreradev/filter/vue';
const users = ref([...]);
const searchTerm = ref('');
// Basic filtering
const { filtered, isFiltering } = useFilter(users, { active: true });
// Debounced filtering
const { filtered: searchResults, isPending } = useDebouncedFilter(
users,
{ name: { $contains: searchTerm } },
{ delay: 300 }
);Svelte Types
import type { Readable, Writable } from 'svelte/store';
type MaybeStore<T> = T | Readable<T> | Writable<T>;
interface UseFilterResult<T> {
filtered: Readable<T[]>;
isFiltering: Readable<boolean>;
}
interface UseFilteredStateResult<T> {
data: Writable<T[]>;
expression: Writable<Expression<T>>;
filtered: Readable<T[]>;
isFiltering: Readable<boolean>;
}
interface UseDebouncedFilterResult<T> {
filtered: Readable<T[]>;
isFiltering: Readable<boolean>;
isPending: Readable<boolean>;
}
interface UsePaginatedFilterResult<T> {
filtered: Readable<T[]>;
isFiltering: Readable<boolean>;
currentPage: Writable<number>;
totalPages: Readable<number>;
pageSize: Writable<number>;
totalItems: Readable<number>;
hasNextPage: Readable<boolean>;
hasPreviousPage: Readable<boolean>;
nextPage: () => void;
previousPage: () => void;
goToPage: (page: number) => void;
setPageSize: (size: number) => void;
}Example:
import { writable } from 'svelte/store';
import { useFilter } from '@mcabreradev/filter/svelte';
const users = writable([...]);
const searchTerm = writable('');
const { filtered, isFiltering } = useFilter(
users,
{ name: { $contains: searchTerm } }
);Type Guards
Expression Type Guard
function isExpression<T>(value: unknown): value is Expression<T> {
return (
typeof value === 'function' ||
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean' ||
(typeof value === 'object' && value !== null && !Array.isArray(value))
);
}Operator Expression Type Guard
function isOperatorExpression<T>(
value: unknown
): value is OperatorExpression<T> {
if (typeof value !== 'object' || value === null) return false;
const operators = [
// Comparison
'$eq', '$ne', '$gt', '$gte', '$lt', '$lte',
// Array
'$in', '$nin', '$contains', '$size',
// String
'$regex', '$match', '$startsWith', '$endsWith',
// Geospatial
'$near', '$geoBox', '$geoPolygon',
// Datetime
'$recent', '$upcoming', '$dayOfWeek', '$timeOfDay', '$age',
'$isWeekday', '$isWeekend', '$isBefore', '$isAfter'
];
return Object.keys(value).some(key => operators.includes(key));
}Logical Expression Type Guard
function isLogicalExpression<T>(
value: unknown
): value is LogicalExpression<T> {
if (typeof value !== 'object' || value === null) return false;
return '$and' in value || '$or' in value || '$not' in value;
}GeoPoint Type Guard
function isGeoPoint(value: unknown): value is GeoPoint {
return (
typeof value === 'object' &&
value !== null &&
'lat' in value &&
'lng' in value &&
typeof (value as GeoPoint).lat === 'number' &&
typeof (value as GeoPoint).lng === 'number' &&
(value as GeoPoint).lat >= -90 &&
(value as GeoPoint).lat <= 90 &&
(value as GeoPoint).lng >= -180 &&
(value as GeoPoint).lng <= 180
);
}Example:
const value = { lat: 40.7128, lng: -74.0060 };
if (isGeoPoint(value)) {
// TypeScript knows value is GeoPoint
console.log(value.lat, value.lng);
}Utility Types
DeepPartial<T>
Make all properties and nested properties optional:
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};NestedKeyOf<T>
Get all possible nested property paths:
type NestedKeyOf<T> = {
[K in keyof T & string]: T[K] extends object
? `${K}` | `${K}.${NestedKeyOf<T[K]>}`
: `${K}`;
}[keyof T & string];
interface User {
profile: {
address: {
city: string;
};
};
}
type UserKeys = NestedKeyOf<User>;Filterable
Constraint for filterable types:
type Filterable = Record<string, any>;
type FilterableArray<T extends Filterable> = T[];Type-Safe Patterns
Builder Pattern
class FilterBuilder<T> {
private expression: Expression<T> = {};
where<K extends keyof T>(
key: K,
operator: OperatorExpression<T[K]>
): this {
this.expression[key] = operator;
return this;
}
and(expressions: Expression<T>[]): this {
this.expression = {
$and: expressions
} as Expression<T>;
return this;
}
build(): Expression<T> {
return this.expression;
}
}
const filter = new FilterBuilder<User>()
.where('age', { $gte: 18 })
.where('status', { $eq: 'active' })
.build();Factory Pattern
function createFilterFactory<T>() {
return {
equals<K extends keyof T>(key: K, value: T[K]): Expression<T> {
return { [key]: { $eq: value } } as Expression<T>;
},
greaterThan<K extends keyof T>(
key: K,
value: T[K] extends number | Date ? T[K] : never
): Expression<T> {
return { [key]: { $gt: value } } as Expression<T>;
},
contains<K extends keyof T>(
key: K,
value: T[K] extends Array<infer U> ? U : never
): Expression<T> {
return { [key]: { $contains: value } } as Expression<T>;
}
};
}
const userFilters = createFilterFactory<User>();
const expression = userFilters.equals('status', 'active');Type Inference Examples
Automatic Type Inference
TypeScript automatically infers types from your data:
const users = [
{ id: 1, name: 'John', age: 25, location: { lat: 40.7128, lng: -74.0060 } },
{ id: 2, name: 'Jane', age: 30, location: { lat: 51.5074, lng: -0.1278 } }
];
// TypeScript infers User type from the array
const { filtered } = useFilter(users, {
age: { $gte: 18 },
location: {
$near: {
center: { lat: 40.7128, lng: -74.0060 },
maxDistanceMeters: 5000
}
}
});
// filtered is inferred as typeof users[0][]Explicit Type Parameters
Provide explicit types for better type safety:
interface User {
id: number;
name: string;
age: number;
location: GeoPoint;
birthDate: Date;
tags: string[];
}
const { filtered } = useFilter<User>([], {
age: { $gte: 18 },
birthDate: { $age: { min: 18, unit: 'years' } },
location: { $near: { center: { lat: 40, lng: -74 }, maxDistanceMeters: 5000 } },
tags: { $contains: 'premium' }
});
// filtered is User[]Intelligent Operator Autocomplete
TypeScript suggests only valid operators for each property type:
interface Product {
id: number; // number property
name: string; // string property
price: number; // number property
tags: string[]; // array property
location: GeoPoint; // GeoPoint property
createdAt: Date; // Date property
inStock: boolean; // boolean property
}
const expression: Expression<Product> = {
// For number properties, TypeScript suggests:
// $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin
id: { $gte: 100 },
price: { $gte: 50, $lte: 500 },
// For string properties, TypeScript suggests:
// $eq, $ne, $startsWith, $endsWith, $contains, $regex, $match, $in, $nin
name: { $startsWith: 'Pro' },
// For array properties, TypeScript suggests:
// $contains, $size, $in, $nin
tags: { $contains: 'electronics' },
// For GeoPoint properties, TypeScript suggests:
// $near, $geoBox, $geoPolygon
location: {
$near: {
center: { lat: 40.7128, lng: -74.0060 },
maxDistanceMeters: 10000
}
},
// For Date properties, TypeScript suggests:
// $recent, $upcoming, $dayOfWeek, $timeOfDay, $age,
// $isWeekday, $isWeekend, $isBefore, $isAfter, $eq, $ne, $gt, $gte, $lt, $lte
createdAt: {
$recent: { days: 30 }
},
// For boolean properties, TypeScript suggests:
// $eq, $ne
inStock: { $eq: true }
};Array OR Syntax (v5.5.0+)
TypeScript properly types array OR syntax:
const expression: Expression<Product> = {
// Array of values - TypeScript knows this is OR logic
id: [1, 2, 3], // id === 1 OR id === 2 OR id === 3
name: ['Laptop', 'Mouse'], // name === 'Laptop' OR name === 'Mouse'
// Works with any property type
price: [99, 199, 299],
inStock: [true]
};Generic Components
Create type-safe reusable components:
interface FilterListProps<T> {
data: T[];
expression: Expression<T>;
renderItem: (item: T) => React.ReactNode;
}
function FilterList<T>({ data, expression, renderItem }: FilterListProps<T>) {
const { filtered } = useFilter<T>(data, expression);
return (
<div>
{filtered.map((item, index) => (
<div key={index}>{renderItem(item)}</div>
))}
</div>
);
}
// Usage with type inference
<FilterList
data={users}
expression={{ age: { $gte: 18 } }}
renderItem={(user) => <div>{user.name}</div>}
/>Type Safety Best Practices
1. Always Provide Type Parameters for Empty Arrays
// ❌ Bad - TypeScript can't infer the type
const { filtered } = useFilter<User>([], expression);
// ✅ Good - Explicit type parameter
const { filtered } = useFilter<User>([], { age: { $gte: 18 } });2. Use Interfaces Over Type Aliases
// ✅ Good - Interfaces are more flexible
interface User {
id: number;
name: string;
location: GeoPoint;
}
// ❌ Avoid - Type aliases are less extensible
type User = {
id: number;
name: string;
location: GeoPoint;
};3. Avoid any and unknown
// ❌ Bad - Loses type safety
const expression: Expression<any> = { ... };
// ✅ Good - Maintains type safety
const expression: Expression<User> = { ... };4. Use Type Guards
// ✅ Good - TypeScript knows the type after guard
if (isOperatorExpression(value)) {
// TypeScript knows value is OperatorExpression
console.log(value.$eq);
}
if (isGeoPoint(location)) {
// TypeScript knows location is GeoPoint
console.log(location.lat, location.lng);
}5. Leverage Type Inference with satisfies
// ✅ Good - Type checking with inference
const expression = {
age: { $gte: 18 },
location: {
$near: {
center: { lat: 40.7128, lng: -74.0060 },
maxDistanceMeters: 5000
}
}
} satisfies Expression<User>;6. Use Const Assertions for Literal Types
// ✅ Good - Preserves literal types
const operators = ['$eq', '$ne', '$gt'] as const;
type Operator = typeof operators[number];
const weekdays = [1, 2, 3, 4, 5] as const;
const expression: Expression<Event> = {
date: { $dayOfWeek: [...weekdays] }
};7. Define Reusable Type Helpers
// Create type helpers for common patterns
type AgeFilter<T extends { age: number }> = Expression<T>;
type LocationFilter<T extends { location: GeoPoint }> = Expression<T>;
type DateFilter<T extends { date: Date }> = Expression<T>;
// Use in your application
const ageFilter: AgeFilter<User> = { age: { $gte: 18 } };
const locationFilter: LocationFilter<Store> = {
location: {
$near: {
center: { lat: 40, lng: -74 },
maxDistanceMeters: 5000
}
}
};Common Type Errors
Error: Property does not exist
interface User {
name: string;
}
// ❌ Error: Property 'age' does not exist on type 'User'
const expression: Expression<User> = {
age: { $gte: 18 }
};Solution: Add the property to the interface or use a different property.
interface User {
name: string;
age: number; // ✅ Add missing property
}
const expression: Expression<User> = {
age: { $gte: 18 } // ✅ Now works
};Error: Type is not assignable
interface User {
name: string;
}
// ❌ Error: Type 'string' is not assignable to type 'never'
const expression: Expression<User> = {
name: { $gt: 'John' } // $gt only works with number or Date
};Solution: Use the correct operator for the type.
const expression: Expression<User> = {
name: { $startsWith: 'John' } // ✅ Use string operators
};Error: Invalid operator for type
interface Product {
tags: string[];
}
// ❌ Error: Property '$startsWith' does not exist
const expression: Expression<Product> = {
tags: { $startsWith: 'electronics' } // $startsWith doesn't work with arrays
};Solution: Use array-specific operators.
const expression: Expression<Product> = {
tags: { $contains: 'electronics' } // ✅ Use array operators
};Error: Incorrect geospatial query structure
interface Store {
location: GeoPoint;
}
// ❌ Error: Property 'center' is missing
const expression: Expression<Store> = {
location: {
$near: {
maxDistanceMeters: 5000
}
}
};Solution: Provide complete query structure.
const expression: Expression<Store> = {
location: {
$near: {
center: { lat: 40.7128, lng: -74.0060 }, // ✅ Add center
maxDistanceMeters: 5000
}
}
};Error: Generic type requires type argument
// ❌ Error: Generic type 'useFilter' requires 1 type argument(s)
const { filtered } = useFilter(data, expression);Solution: Provide explicit type parameter when type can't be inferred.
// ✅ Option 1: Provide type parameter
const { filtered } = useFilter<User>(data, expression);
// ✅ Option 2: Use typed data
const users: User[] = data;
const { filtered } = useFilter(users, expression);Error: Invalid Datetime query
interface Event {
date: Date;
}
// ❌ Error: Type '{ invalid: number }' is not assignable
const expression: Expression<Event> = {
date: {
$recent: { invalid: 7 }
}
};Solution: Use correct RelativeTimeQuery properties.
const expression: Expression<Event> = {
date: {
$recent: { days: 7 } // ✅ Use days, hours, or minutes
}
};