Object types
Object types, the backing model behind each one, and the different ways to define them.
Defining an object type
interface ICharacter {
id: string;
name: string;
birthYear?: string;
biography?: string;
editorId: string;
}
const Character = builder.objectRef<ICharacter>('Character');
Character.implement({
fields: (t) => ({
id: t.exposeID('id'),
name: t.exposeString('name'),
}),
});Each tab defines the same Character object type. To define one we give it a name, a backing model (the TypeScript shape of the data behind the type), and the fields that make it up. In the objectRef form shown first, builder.objectRef<ICharacter>('Character') creates a reference that carries the name and the backing model, and .implement supplies the fields. Each field maps data from the backing model to the GraphQL shape a client sees: t.exposeID('id') and t.exposeString('name') read those two properties straight off the backing model, and the Fields guide covers the field builder in detail. The Classes and Builder types tabs build the same type from a class and from a registered name, covered below.
Type references
builder.objectRef returns a type reference: a value that stands in for the Character type wherever the schema needs to name it. A reference is not particular to objectRef. builder.objectType returns one too, a class used to define a type acts as its own reference, and a type name registered on the builder (the Builder types tab) references the type by string. Any of these can be used as a field's type, which is how a field on one type returns another type.
builder.objectType takes a reference in each of these forms as its first argument: a class, an existing object ref, or a string name registered on the builder's Objects generic. implement is the object-ref shorthand: calling .implement(options) on a ref is the same as passing that ref and the options to builder.objectType.
The backing model
The generic on objectRef<ICharacter> (or the class's instance type, or the shape registered under a name) is the type's backing model: the value your resolvers return for the type, and the parent Pothos hands to every field defined on it. Whatever form the reference takes, it carries this shape, and Pothos checks the type's fields against it. t.exposeString('name') compiles only when name is a string on ICharacter, and any field that returns Character has to resolve to a value matching it.
The backing model and the GraphQL type are separate things. The backing model is whatever your resolvers work with (a database row, a plain object, a class instance, or even just a string id), while the GraphQL type is the set of fields a client can select. Exposing a property with t.expose* is the direct case, where a field reads a value straight off the backing model. Every other field is defined by writing a resolver, and as long as you can compute a field's value from the backing model (and the context), you can add it to the type without changing the backing model. A type backed by nothing but an id can still present a full set of fields, each resolver loading what it needs. The Fields guide covers computed fields and the field builder.
Object type options
builder.objectRef<ICharacter>('Character') takes two things: the type's name and, as its generic, the backing model. Everything else about the type goes to implement: the fields callback, a description, the interfaces the type implements, and an isTypeOf function for resolving abstract types. fields is the option you pass most often, but it works like any other option on implement.
When a type doesn't need to be referenced before it's implemented, the two calls chain into a single expression:
const Race = builder.objectRef<IRace>('Race').implement({
fields: (t) => ({
id: t.exposeID('id'),
name: t.exposeString('name'),
lifespan: t.exposeString('lifespan'),
}),
});builder.objectRef<IRace>('Race').implement({ ... }) creates the reference and implements it in one statement. Keeping the two calls apart, as Character does above, is what lets two types reference each other: the reference exists before either type's fields are defined, so each can name the other. The Circular references pattern covers that case.
Returning an object type
A type reference can be the type of any field, on any type in the schema. Here the root Query type returns a list of Character:
builder.queryType({
fields: (t) => ({
characters: t.field({
type: [Character],
resolve: () => characters,
}),
}),
});Wrapping the reference in an array (type: [Character]) makes the field a list, while a bare type: Character returns a single one. Because the field returns Character, its resolver has to return values matching the backing model, and TypeScript reports an error if it doesn't. The Queries guide covers the Query root itself.
Definition styles
The three tabs at the top of the page define the same Character type in the three reference forms. All are fully supported, and a single schema can mix them freely.
Object refs. builder.objectRef<ICharacter>('Character') states the backing model as a generic and hands back a reference to pass around.
Classes. builder.objectType(Character, { ... }) defines the type from a class and uses the class's instance type as the backing model, so parent in every resolver is an instance of the class, methods and getters included. This fits well when your app already keeps classes for its data. Because a class is also a value at runtime, you can write isTypeOf: (value) => value instanceof Character, an instanceof check that identifies the type when an interface or union value is resolved (isTypeOf is always an option you set; it isn't derived from the class).
Builder types. Registering a type on the builder's Objects generic maps a name to its backing model, so you can refer to the type by that string name (t.field({ type: 'Character' })) anywhere in the schema. This keeps your type definitions in one place instead of importing a reference into every file that uses them. Registering the name only tells TypeScript about the type; you still create it at runtime with builder.objectType('Character', { ... }).