Recommended Usage for GraphQL Interfaces
Explore examples and avoid common pitfalls
GraphQL interfaces enable schema fields to return one of multiple object types, all of which must implement that interface. To implement an interface, an object type must define all fields that are included in that interface (otherwise, the schema is invalid):
1interface Media {
2 title: String!
3}
4
5type Book implements Media {
6 title: String!
7 author: String!
8}
Because interfaces can enforce this requirement on implementing object types, it can be tempting to create interfaces purely to enforce field definitions. The following schema requires a name
field on all its object types by declaring that those types implement the Nameable
interface:
❌
1interface Nameable {
2 name: String
3}
4
5type Cat implements Nameable {
6 name: String
7 meowVolume: Int
8}
9
10type Dog implements Nameable {
11 name: String
12 barkVolume: Int
13}
14
15type Owner implements Nameable {
16 name: String
17 cats: [Cat]
18 dogs: [Dog]
19}
20
21type Query {
22 owners: [Owner]
23}
Nameable
is never used as the return type for a field. This means that clients have no way to take advantage of this polymorphic relationship in their operations.The lack of valid client use cases indicates a code smell. The Nameable
interface is unnecessary, and it might cause problems when making changes to your schema in the future. And because distributing an interface across subgraphs presents its own challenges, removing unnecessary interfaces usually makes life simpler.
In the next example, we specify that both the Cat
and Dog
types implement the Pet
interface so that we can return a polymorphic list of both Cats and Dogs in the Owner.pets
field. Now that there's a valid client use case for polymorphism, using an interface is justified.
✅
1interface Pet {
2 name: String
3}
4
5type Cat implements Pet {
6 name: String
7 meowVolume: Int
8}
9
10type Dog implements Pet {
11 name: String
12 barkVolume: Int
13}
14
15type Owner {
16 name: String
17 pets: [Pet]
18}
19
20type Query {
21 owners: [Owner]
22}
Implications of adding new implementing types
When a client includes a field returning an abstract type in an operation, they usually enumerate which fields from the concrete types they're interested in with "conditional fragments," like so:
1query OwnersAndPets {
2 owners {
3 name
4 pets {
5 name
6 ... on Cat {
7 meowVolume
8 }
9 ... on Dog {
10 barkVolume
11 }
12 }
13 }
14}
Pet
were a union instead of an interface (union Pet = Cat | Dog
), then all requested fields would need to be included in conditional fragments. It wouldn't be possible to include the name
field outside of a fragment, even though the two types both happen to define that field.If your API adds additional possible types to the abstract type, clients must change their operations to select data from those new types. Although this isn't a breaking change, this situation warrants some defensive programming.
1// Trigger a TypeScript error if a new type is introduced but
2// we haven't added a switch case for it.
3const assertUnknown = (x: never) =>
4 console.log(`Unknown Pet type: ${(x as any).__typename}`);
5
6switch (pet.__typename) {
7 case 'Cat':
8 console.log(`Cat meows ${pet.meowVolume}`);
9 break;
10 case 'Dog':
11 console.log(`Dog barks ${pet.barkVolume}`);
12 break;
13 default:
14 assertUnknown(pet);
15 console.log('Fallback behavior');
16}
graphql-code-generator
, accessing known interface fields like Pet.name
will trigger a TypeScript error. See issue 8538 for more discussion.1// `when` must be used as an expression to require exhaustivity
2val result = when (pet.__typename) {
3 "Cat" -> println("Cat meows ${pet.onCat?.meowVolume}")
4 "Dog" -> println("Dog barks ${pet.onDog?.barkVolume}")
5 else -> println("Unknown animal ${pet.__typename}")
6}
1switch pet.__typename {
2case "Cat":
3 print("Cat meows \(pet.asCat?.meowVolume ?? 0)")
4case "Dog":
5 print("Dog barks \(pet.asDog?.barkVolume ?? 0)")
6default:
7 print("Unknown animal \(pet.__typename)")
8}