InteractiveFrameworks

Interfaces and unions

Fields that hold more than one kind of object: an Animal interface that cats and dogs implement, with a field resolved once for both, a search that answers a union of cats, dogs and owners, and the resolveType functions that tell plain objects apart.

What you'll learn

  • Declare an interface with @InterfaceType() and object types that implement it, inheriting its fields
  • Query a list of an interface with inline fragments and __typename
  • Resolve a field once for every implementation with a resolver for the interface
  • Declare a union with createUnionType(), and tell plain objects apart with resolveType

The shelter takes dogs now. A dog has a name and an age like a cat, but a size instead of a breed. The front page lists every animal, and the search box finds animals and owners alike. With object types alone, that means one query per type and clients stitching the answers together. GraphQL has two abstract types for "one of several kinds": an interface, a set of fields that several object types share and implement, and a union, a list of object types with nothing in common. A field typed with either can hold any of its members, and the client asks for each member's own fields with inline fragments.

Interfaces

An interface is an abstract class with @InterfaceType(). Its fields carry @Field(), and object types implement it with the implements option:

@InterfaceType({ resolveType: (item) => ('pages' in item ? Book : Film) })
export abstract class Item {
  @Field() title: string;
}

@ObjectType({ implements: () => [Item] })
export class Book implements Item {
  title: string;
  @Field(() => Int) pages: number;
}

The implementing class inherits the interface's fields in the schema, so it does not decorate them again. The TypeScript implements only checks that the properties exist.

A query typed [Item] can return books and films, and the client selects the shared fields directly and each type's own fields in a fragment:

{ items { title __typename ... on Book { pages } ... on Film { minutes } } }

__typename is a field every object has: the name of the concrete type. Asking for pages outside the fragment is refused: Cannot query field "pages" on type "Item". Did you mean to use an inline fragment on "Book"?

Deciding which type a value is

For every value in an abstract field, GraphQL needs to know its concrete type. Nest's default checks instanceof against the implementing classes, which works when your service returns new Book(). Services that return plain objects, as a database or an HTTP client does, need a resolveType function: it receives the value and returns the class. Without it, every answer fails with Return type for "Item" cannot be resolved. If you did not pass a custom implementation (the "resolveType" function), you must return an instance of a class instead of a plain JavaScript object.

A resolver can also add fields to an interface itself: @Resolver(() => Item) with a @ResolveField() gives every implementation the field, resolved once. The interface's resolver may declare the queries that return it too.

Unions

A union is declared with createUnionType(), from types that share nothing:

export const ShelfEntry = createUnionType({
  name: 'ShelfEntry',
  types: () => [Book, Film, Author] as const,
  resolveType: (value) => ('pages' in value ? Book : 'minutes' in value ? Film : Author),
});

A query returns it with @Query(() => [ShelfEntry]). A union has no fields of its own, not even shared ones, so every field is asked for in a fragment. A fragment may name an interface its members implement: ... on Item { title } covers books and films at once.

Your task

AnimalsService returns cats and dogs as plain objects, and search(term) returns animals and owners whose name contains the term. Owner is an object type already.

  1. Make Animal an interface with an integer id, a name and an integer age. An object with a breed is a Cat, any other a Dog.
  2. Make Cat and Dog object types implementing Animal, each keeping its own field.
  3. Make SearchResult a union of Cat, Dog and Owner: an object with a breed is a Cat, one with a size a Dog, anything else an Owner.
  4. In AnimalsResolver, a resolver for Animal: an animals query returning every animal, a summary field on every animal (<name>, <age> years old), and a search(term) query returning SearchResults.

When it fails

  • Query root type must be provided. in the console: the resolver declares no query yet, so the schema has none.
  • Return type for "Animal" cannot be resolved: the interface has no resolveType and the service returns plain objects. The same message names SearchResult when the union lacks one.
  • Runtime Object type "Dog" is not a possible type for "Animal".: resolveType returned Dog, but Dog does not implement Animal.
  • Cannot query field "breed" on type "Animal": ask for type-specific fields inside ... on Cat.

Remember

  • An interface is shared fields; implementations inherit them with implements: () => [Interface].
  • A union is several types with nothing shared; everything is asked for in fragments.
  • Plain objects need resolveType; __typename shows what it decided.
  • A resolver for the interface resolves a field once for every implementation.
Stuck? Show a hint

animal.interface.ts: @InterfaceType({ resolveType: (animal) => ('breed' in animal ? Cat : Dog) }) with @Field on id, name and age. cat.model.ts and dog.model.ts: @ObjectType({ implements: () => [Animal] }), the inherited properties undecorated. search-result.union.ts: createUnionType({ name: 'SearchResult', types: () => [Cat, Dog, Owner] as const, resolveType }). The resolver: @Resolver(() => Animal), @Query(() => [Animal]), @ResolveField(() => String) summary(@Parent() animal), @Query(() => [SearchResult]) search(@Args('term') term).