Type-Safe Directus Filters with GraphQL Codegen
by Abdelkader Settah
May 08, 2026
Type-Safe Directus Filters with GraphQL Codegen
If you’re using Directus with GraphQL codegen, your queries are typed but your filters often aren’t. You end up with any shapes leaking through, autocompletion stops working, and a typo silently breaks production.
This post shows the pattern I use to keep filter conditions type-safe and composable.
The starting point
If you already have codegen set up (@graphql-codegen/cli with the client preset against your Directus GraphQL endpoint), each collection produces a *_filter type. For an articles collection, codegen generates an articles_filter shape that mirrors what Directus accepts.
// generated by codegen
type articles_filter = {
status?: { _eq?: string; _in?: string[] };
date_published?: { _gte?: string; _lte?: string };
title?: { _contains?: string };
// ...
};
The trouble starts the moment you compose these filters in app code.
The problem
Most filter-builder code I see looks like this:
function buildArticleFilter(opts: { search?: string; status?: string }) {
const conditions: any[] = []; // <- here's the leak
if (opts.status) conditions.push({ status: { _eq: opts.status } });
if (opts.search) conditions.push({ title: { _contains: opts.search } });
return conditions.length ? { _and: conditions } : {};
}
Two issues with this:
conditions: any[]kills type checking inside the conditions array.- A typo like
_qeinstead of_eqwon’t fail until runtime, when the query just returns empty results.
The fix
Pull the filter type from your generated schema and use it everywhere you touch a condition:
import type { Articles_Filter } from '@/generated/directus/graphql';
export function buildArticleFilter(opts: {
search?: string;
status?: 'published' | 'draft';
}): Articles_Filter {
const conditions: Articles_Filter[] = [];
if (opts.status) conditions.push({ status: { _eq: opts.status } });
if (opts.search) conditions.push({ title: { _contains: opts.search } });
return conditions.length ? { _and: conditions } : {};
}
Now _qe is a compile error, and the IDE autocompletes every operator and field.
Making it composable
If you build filters across many collections, the boilerplate gets repetitive. A small generic helper cleans it up:
function combineAnd<F>(parts: (F | null | undefined)[]): F | Record<string, never> {
const filtered = parts.filter(Boolean) as F[];
if (filtered.length === 0) return {};
if (filtered.length === 1) return filtered[0];
return { _and: filtered } as unknown as F;
}
Used per collection:
import type { Articles_Filter } from '@/generated/directus/graphql';
export function buildArticleFilter(opts: {
search?: string;
status?: 'published' | 'draft';
}) {
return combineAnd<Articles_Filter>([
opts.status ? { status: { _eq: opts.status } } : null,
opts.search ? { title: { _contains: opts.search } } : null,
]);
}
The collection type stays specific. The helper just removes the _and ceremony.
Why does this matter?
Two reasons.
First, Directus filter shapes change when your schema changes. With typed filters, a removed field becomes a TypeScript error in your filter helpers, not a silent 200 with empty results.
Second, filters tend to grow. Search, status, category, date range, author, tags. Without types, that grows into a maintenance liability fast. With types, the IDE keeps you honest.
Conclusion
Pull the *_filter types from your codegen output and use them in every helper that builds filter conditions. The change is small, but it removes a class of silent bugs and makes the filter logic safe to refactor.
Summary
Codegen generates filter types alongside your queries. Type your filter-builder return values and condition arrays with the generated *_filter shapes. Add a small combineAnd helper if you build filters in more than one place. Your IDE catches typos at compile time, and your filter logic stays trustworthy as the schema evolves.
If your team is fighting type drift between CMS and frontend, I help product teams set up codegen pipelines and type architecture that won’t rot. See my TypeScript consulting page.