Skip to content
On this page

Directus SDK Types

The Directus SDK provides TypeScript types used for auto-completion and generating output item types, but these can be complex to work with. This guide will cover some approaches to more easily work with these types.

This guide assumes you're working with the Directus SDK and using TypeScript 5 or later.

Setting Up a Schema

A schema contains a root schema type, custom fields on core collections, and a defined type for each available collection and field.

The root Schema type

The root schema type is the type provided to the SDK client. It should contain all available collections, including junction collections for many-to-many and many-to-any relations. This type is used by the SDK as a lookup table to determine what relations exist.

If a collection if defined as an array, it is considered a regular collection with multiple items. If not defined as an array, but instead as a single type, the collection is considered to be a singleton.

ts
interface MySchema {
	// regular collections are array types
	collection_a: CollectionA[];
	// singleton collections are singular types
	collection_c: CollectionC;
}
interface MySchema {
	// regular collections are array types
	collection_a: CollectionA[];
	// singleton collections are singular types
	collection_c: CollectionC;
}

Improving Results

For the most reliable results, the root schema types should be kept as pure as possible. This means avoiding unions (CollectionA | null), optional types (optional_collection?: CollectionA[]), and preferably inline relational types (types nested directly on the root schema) except when adding custom fields to core collections.

Custom Fields on Core Collections

Core collections are provided and required in every Directus project, and are prefixed with directus_. Directus projects can add additional fields to these collections, but should also be included in the schema when initializing the Directus SDK.

To define custom fields on the core collections, add a type containing only your custom fields as a singular type.

ts
interface MySchema {
	// regular collections are array types
	collection_a: CollectionA[];
	// singleton collections are singular types
	collection_c: CollectionC;
	// extend the provided DirectusUser type 
	directus_users: CustomUser; 
}

interface CustomUser { 
	custom_field: string; 
} 
interface MySchema {
	// regular collections are array types
	collection_a: CollectionA[];
	// singleton collections are singular types
	collection_c: CollectionC;
	// extend the provided DirectusUser type 
	directus_users: CustomUser; 
}

interface CustomUser { 
	custom_field: string; 
} 

🪲 Core Collections Typing Bug

There is currently an unsolved bug when using Directus core collections. If you need to include types for core collections, you will need to add an item to the root type starting with directus_ for any other core collections to be correctly typed.

When this bug is resolved, you may need to remove this temporary fix.

Collection Field Types

Most Directus field types will map to one of the TypeScript primitive types (string, number, boolean). There are some exceptions to this where literal types are used to distinguish between primitives in order to add extra functionality.

ts
interface CollectionA {
	id: number;
	status: string;
	toggle: boolean;
}
interface CollectionA {
	id: number;
	status: string;
	toggle: boolean;
}

There are currently 3 literal types that can be applied. The first 2 are both used to apply the count(field)array function in the filter/field auto-complete suggestions, these are the 'json' and 'csv' string literal types. The 'datetime' string literal type which is used to apply all datetime functions in the filter/field auto-complete suggestions.

ts
interface CollectionA {
	id: number;
	status: string;
	toggle: boolean;

	tags: 'csv'; 
	json_field: 'json'; 
	date_created: 'datetime'; 
}
interface CollectionA {
	id: number;
	status: string;
	toggle: boolean;

	tags: 'csv'; 
	json_field: 'json'; 
	date_created: 'datetime'; 
}

In the output types these string literals will get resolved to their appropriate types:

  • 'csv' resolves to string[]
  • 'datetime' resolves to string
  • 'json' resolves to JsonValue

Types to Avoid

Some types should be avoided in the Schema as they may not play well with the type logic: any or any[], empty type {}, never or void.

Adding Relational Fields

For regular relations without junction collections, define a relation using a union of the primary key type and the related object.

ts
interface MySchema {
	// regular collections are array types
	collection_a: CollectionA[];
	collection_b: CollectionB[]; 
	// singleton collections are singular types
	collection_c: CollectionC;
	// extend the provided DirectusUser type
	directus_users: CustomUser;
}

interface CollectionB { 
	id: string; 
} 
interface MySchema {
	// regular collections are array types
	collection_a: CollectionA[];
	collection_b: CollectionB[]; 
	// singleton collections are singular types
	collection_c: CollectionC;
	// extend the provided DirectusUser type
	directus_users: CustomUser;
}

interface CollectionB { 
	id: string; 
} 

Many to One

ts
interface CollectionB {
	id: string;
	m2o: number | CollectionA; 
}
interface CollectionB {
	id: string;
	m2o: number | CollectionA; 
}

One to Many

ts
interface CollectionB {
	id: string;
	m2o: number | CollectionA;
	o2m: number[] | CollectionA[]; 
}
interface CollectionB {
	id: string;
	m2o: number | CollectionA;
	o2m: number[] | CollectionA[]; 
}

Working with Junction Collections

For relations that rely on a junction collection, define the junction collection on the root schema and refer to this new type similar to the one to many relation above.

Many to Many

ts
interface MySchema {
	// regular collections are array types
	collection_a: CollectionA[];
	collection_b: CollectionB[];
	// singleton collections are singular types
	collection_c: CollectionC;
	// many-to-many junction collection 
	collection_b_a_m2m: CollectionBA_Many[]; 
	// extend the provided DirectusUser type
	directus_users: CustomUser;
}

// many-to-many junction table 
interface CollectionBA_Many { 
	id: number; 
	collection_b_id: string | CollectionB; 
	collection_a_id: number | CollectionA; 
} 

interface CollectionB {
	id: string;
	m2o: number | CollectionA;
	o2m: number[] | CollectionA[];
	m2m: number[] | CollectionBA_Many[]; 
}
interface MySchema {
	// regular collections are array types
	collection_a: CollectionA[];
	collection_b: CollectionB[];
	// singleton collections are singular types
	collection_c: CollectionC;
	// many-to-many junction collection 
	collection_b_a_m2m: CollectionBA_Many[]; 
	// extend the provided DirectusUser type
	directus_users: CustomUser;
}

// many-to-many junction table 
interface CollectionBA_Many { 
	id: number; 
	collection_b_id: string | CollectionB; 
	collection_a_id: number | CollectionA; 
} 

interface CollectionB {
	id: string;
	m2o: number | CollectionA;
	o2m: number[] | CollectionA[];
	m2m: number[] | CollectionBA_Many[]; 
}

Many to Any

ts
interface MySchema {
	// regular collections are array types
	collection_a: CollectionA[];
	collection_b: CollectionB[];
	// singleton collections are singular types
	collection_c: CollectionC;
	// many-to-many junction collection
	collection_b_a_m2m: CollectionBA_Many[];
	// many-to-any junction collection 
	collection_b_a_m2a: CollectionBA_Any[]; 
	// extend the provided DirectusUser type
	directus_users: CustomUser;
}

// many-to-any junction table 
interface CollectionBA_Any { 
	id: number; 
	collection_b_id: string | CollectionB; 
	collection: 'collection_a' | 'collection_c'; 
	item: string | CollectionA | CollectionC; 
} 

interface CollectionB {
	id: string;
	m2o: number | CollectionA;
	o2m: number[] | CollectionA[];
	m2m: number[] | CollectionBA_Many[];
	m2m: number[] | CollectionBA_Any[]; 
}
interface MySchema {
	// regular collections are array types
	collection_a: CollectionA[];
	collection_b: CollectionB[];
	// singleton collections are singular types
	collection_c: CollectionC;
	// many-to-many junction collection
	collection_b_a_m2m: CollectionBA_Many[];
	// many-to-any junction collection 
	collection_b_a_m2a: CollectionBA_Any[]; 
	// extend the provided DirectusUser type
	directus_users: CustomUser;
}

// many-to-any junction table 
interface CollectionBA_Any { 
	id: number; 
	collection_b_id: string | CollectionB; 
	collection: 'collection_a' | 'collection_c'; 
	item: string | CollectionA | CollectionC; 
} 

interface CollectionB {
	id: string;
	m2o: number | CollectionA;
	o2m: number[] | CollectionA[];
	m2m: number[] | CollectionBA_Many[];
	m2m: number[] | CollectionBA_Any[]; 
}

Working with Generated Output

ts
async function getCollectionA() {
  return await client.request(
    readItems('collection_a', {
      fields: ['id']
    })
  )
}

// generated type that can be used in the component
// resolves to { "id": number } in this example
type GeneratedType = Awaited<ReturnType<typeof getCollectionA>>;
async function getCollectionA() {
  return await client.request(
    readItems('collection_a', {
      fields: ['id']
    })
  )
}

// generated type that can be used in the component
// resolves to { "id": number } in this example
type GeneratedType = Awaited<ReturnType<typeof getCollectionA>>;

Debugging

The SDK provides some utility generic types to help debug issues. The Identity<> generic type can be used to try and resolve generics to their results. This may not always work, and you may need to reduce the type.

ts
// the output type from the previous example
type GeneratedType = Awaited<ReturnType<typeof getCollectionA>>;

// when hovering over this type it may look unreadable like:
//  Merge<MapFlatFields<object & CollectionA, "id", MappedFunctionFields<MySchema, object & CollectionA>>, {}, MapFlatFields<...>, {}>[]

// should resolve to { id: number; }
type ResolvedType = Identity< GeneratedType[0] >;
// the output type from the previous example
type GeneratedType = Awaited<ReturnType<typeof getCollectionA>>;

// when hovering over this type it may look unreadable like:
//  Merge<MapFlatFields<object & CollectionA, "id", MappedFunctionFields<MySchema, object & CollectionA>>, {}, MapFlatFields<...>, {}>[]

// should resolve to { id: number; }
type ResolvedType = Identity< GeneratedType[0] >;

Working with input Query Types

For the output types to work properly, the fields list needs to be static so the types can read the fields that were selected in the query.

ts
// this does not work and resolves to string[], losing all information about the fields themselves
const fields = ["id", "status"];

// correctly resolves to readonly ["id", "status"]
const fields = ["id", "status"] as const;
// this does not work and resolves to string[], losing all information about the fields themselves
const fields = ["id", "status"];

// correctly resolves to readonly ["id", "status"]
const fields = ["id", "status"] as const;

Complete example:

ts
const query: Query<MySchema, CollectionA> = {
	limit: 20,
	offset: 0,
};

let search = 'test';
if (search) {
	query.search = search;
}

// create a second query for literal/readonly type inference
const query2 =  {
    ...query,
	fields: [
        "id", "status"
	],
} satisfies Query<MySchema, CollectionA>;

const results = await directusClient.request(readItems("collection_a", query2));

// or build the query directly inline
const results2 = await directusClient.request(readItems("collection_a", {
    ...query,
    search,
	fields: [
        "id", "status"
	],
}));
const query: Query<MySchema, CollectionA> = {
	limit: 20,
	offset: 0,
};

let search = 'test';
if (search) {
	query.search = search;
}

// create a second query for literal/readonly type inference
const query2 =  {
    ...query,
	fields: [
        "id", "status"
	],
} satisfies Query<MySchema, CollectionA>;

const results = await directusClient.request(readItems("collection_a", query2));

// or build the query directly inline
const results2 = await directusClient.request(readItems("collection_a", {
    ...query,
    search,
	fields: [
        "id", "status"
	],
}));

Alias Unsupported

At this time, alias has not been typed yet for use in other query parameters like deep.

Your thoughts matter to us

How helpful was this article?

Contributors
Tim de Heiden

Last updated: