SDK Types for Relational Collections

Hi! I am trying to configure my schema in the sdk with the following relations. I followed the Advanced Types with the Directus SDK guide to define the schema.

interface Player {
  id: string;
  name: string;
  pets: string[] | Pet[];
}

interface Pet {
  id: string;
  name: string;
  owner: string | Player;
}

interface Schema {
  players: Player[];
  pets: Pet[];
}

To query the players and list their pets, I use the following request:

const players = directus.request(
  readItems("players", { fields: ["name", { pets: ["name"] }]}
)

When I try to display the first pet’s name, I get a typescript error because pets could according to the schema either be a string or a Pet object.

<div>{ player.pets[0].name }</div>

Property 'name' does not exist on type 'string | { name: string; }'.
Property 'name' does not exist on type 'string'.ts(2339)

How can I fix this problem? To me, it seems like the type should be defined by the fields object of the request. Is there a workaround I could use?

Hi! @mink, I understand the issue you’re facing with TypeScript types when querying related data in Directus. Here are the best solutions:

Solution 1: Let TypeScript Infer Types Automatically (Recommended)
This is the simplest approach. TypeScript will automatically infer the correct types based on your field query:

const players = await directus.request(
  readItems('players', { 
    fields: ['name', { pets: ['name'] }] 
  })
);

// TypeScript now knows pets contains objects with name
const petName = players[0].pets?.[0]?.name; // ✅ No errors!

Solution 2: Create Specific Response Types
Create interfaces that match your expected response structure:

interface PlayerWithPetNames {
  id: string;
  name: string;
  pets: { name: string }[] | null;
}

const players = await directus.request(
  readItems('players', { 
    fields: ['name', { pets: ['name'] }] 
  })
) as PlayerWithPetNames[];

Solution 3: Use Type Guards
Create a helper function to check types at runtime:

function isPetObject(pet: string | Pet): pet is Pet {
  return typeof pet !== 'string' && 'name' in pet;
}

// In your component:
<div>
  {player.pets?.[0] && isPetObject(player.pets[0]) 
    ? player.pets[0].name 
    : 'No pet name'}
</div>

Solution 4: Generic Query Response Type (Advanced)
For complex applications, create a utility type:

type QueryResponse<T> = {
  [K in keyof T]: T[K] extends Array<infer U> 
    ? U extends object 
      ? QueryResponse<U>[] 
      : U[] 
    : T[K];
};

const players = await directus.request(
  readItems('players', { 
    fields: ['name', { pets: ['name'] }] 
  })
);

Recommendation
Start with Solution 1 - it’s the cleanest and most maintainable. TypeScript’s automatic type inference will ensure your types always match what you actually requested in your query.

If you need to use the Player type elsewhere in your app, consider creating separate interfaces for your base schema types and your query response types to avoid conflicts.

// For schema definition
interface PlayerBase {
  id: string;
  name: string;
  pets: string[] | Pet[];
}

// For specific queries
interface PlayerWithPets extends Omit<PlayerBase, 'pets'> {
  pets: { name: string }[];
}

This keeps your types accurate and your code error-free!