← All Blogs

Type-Safe Directus Filters with GraphQL Codegen

Type-safe Directus GraphQL filters

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 _qe instead of _eq won’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.