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.
- Make
Animalan interface with an integerid, anameand an integerage. An object with abreedis aCat, any other aDog. - Make
CatandDogobject types implementingAnimal, each keeping its own field. - Make
SearchResulta union ofCat,DogandOwner: an object with abreedis aCat, one with asizeaDog, anything else anOwner. - In
AnimalsResolver, a resolver forAnimal: ananimalsquery returning every animal, asummaryfield on every animal (<name>, <age> years old), and asearch(term)query returningSearchResults.
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 noresolveTypeand the service returns plain objects. The same message namesSearchResultwhen the union lacks one.Runtime Object type "Dog" is not a possible type for "Animal".:resolveTypereturnedDog, butDogdoes not implementAnimal.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;__typenameshows 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).