Pothos
Guide

Circular References

Two GraphQL types can refer to each other. The problems that sometimes accompany this come from JavaScript module initialization or TypeScript inference. They need different fixes.

Circular imports

If two modules import each other, reading an export before its initialization can produce an undefined value or a ReferenceError, depending on the module system. Pothos defers field callbacks until builder.toSchema(), so references inside those callbacks can work even when the modules import each other.

Keep the builder in a module that does not import your schema definitions. Import all schema modules before calling toSchema(), and do not import that schema entry point from a type definition. See App Layout for a complete file layout.

In this example, user.ts and post.ts import each other, but neither reads the other module's ref until its field callback runs. The backing types use type-only imports, which do not run JavaScript.

// builder.ts
import SchemaBuilder from '@pothos/core';

export const builder = new SchemaBuilder({});
// user.ts
import { builder } from './builder';
import { Post, type PostShape } from './post';

export interface UserShape {
  name: string;
  posts: PostShape[];
}

export const User = builder.objectRef<UserShape>('User');

User.implement({
  fields: (t) => ({
    name: t.exposeString('name'),
    posts: t.expose('posts', { type: [Post] }),
  }),
});
// post.ts
import { builder } from './builder';
import { User, type UserShape } from './user';

export interface PostShape {
  title: string;
  author: UserShape;
}

export const Post = builder.objectRef<PostShape>('Post');

Post.implement({
  fields: (t) => ({
    title: t.exposeString('title'),
    author: t.expose('author', { type: User }),
  }),
});
// schema.ts
import { builder } from './builder';
import { User, type UserShape } from './user';
import './post';

const user: UserShape = { name: 'Alex', posts: [] };
user.posts.push({ title: 'First post', author: user });

builder.queryType({
  fields: (t) => ({
    user: t.field({ type: User, resolve: () => user }),
  }),
});

export const schema = builder.toSchema();

The schema supports queries that follow the relationship in both directions:

{
  user {
    name
    posts {
      title
      author { name }
    }
  }
}

Import values directly from the modules that define them. A barrel file that re-exports every type can introduce extra cycles. An entry point can import type-definition modules for their side effects without re-exporting their values.

Deferring fields does not defer every option. For example, an interfaces: [SomeInterface] array is evaluated immediately. If a cycle reads an uninitialized ref there, change the imports or move the refs into a separate module that does not import their implementations.

TypeScript inference

A chained declaration such as const User = builder.objectRef<UserShape>('User').implement(...) can create an inference cycle when its fields refer back to User, directly or through another type. Declare the ref first, then call User.implement(...) separately, as in the example above. TypeScript can determine the ref's type without inspecting the fields. This breaks the inference dependency; it does not remove the JavaScript import cycle.

You can also pass the declared ref to builder.objectType. As an alternative to the Post.implement call above, define its fields in separate calls:

builder.objectType(Post, {
  fields: (t) => ({ title: t.exposeString('title') }),
});

builder.objectField(Post, 'author', (t) => t.expose('author', { type: User }));

Classes already used by your application and names registered in the builder's Objects or Interfaces SchemaTypes also supply backing types independently of field inference. String names can avoid importing refs between type-definition modules, but all implementations still need to be loaded before toSchema().

Recursive inputs

Input types infer their shape from their fields. For recursive inputs, declare the backing shape and an input ref explicitly, as described in Recursive Inputs.

Plugins that infer backing types from their options may also need explicit types to break an inference cycle; check the relevant plugin's documentation.

On this page