Docs
Launch GraphOS Studio

Recommended usage for GraphQL interfaces

Explore examples and avoid common pitfalls

schema-designfederation

💡 TIP

If you're an enterprise customer looking for more material on this topic, try the

course on .

Not an enterprise customer?

interfaces enable schema to return one of multiple , 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):

interface Media {
title: String!
}
type Book implements Media {
title: String!
author: String!
}

Because interfaces can enforce this requirement on implementing , it can be tempting to create interfaces purely to enforce definitions. The following schema requires a name on all its by declaring that those types implement the Nameable interface:

interface Nameable {
name: String
}
type Cat implements Nameable {
name: String
meowVolume: Int
}
type Dog implements Nameable {
name: String
barkVolume: Int
}
type Owner implements Nameable {
name: String
cats: [Cat]
dogs: [Dog]
}
type Query {
owners: [Owner]
}

⚠️ CAUTION

This is not a recommended use for interfaces! Note that in this schema, Nameable is never used as the return type for a . This means that clients have no way to take advantage of this polymorphic relationship in their .

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 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 . Now that there's a valid client use case for polymorphism, using an interface is justified.

interface Pet {
name: String
}
type Cat implements Pet {
name: String
meowVolume: Int
}
type Dog implements Pet {
name: String
barkVolume: Int
}
type Owner {
name: String
pets: [Pet]
}
type Query {
owners: [Owner]
}

Implications of adding new implementing types

When a client includes a returning an abstract type in an , they usually enumerate which fields from the concrete types they're interested in with "conditional ," like so:

query OwnersAndPets {
owners {
name
pets {
name
... on Cat {
meowVolume
}
... on Dog {
barkVolume
}
}
}
}

NOTE

If Pet were a union instead of an interface (union Pet = Cat | Dog), then all requested would need to be included in conditional . It wouldn't be possible to include the name outside of a , 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 to select data from those new types. Although this isn't a breaking change, this situation warrants some defensive programming.

// Trigger a TypeScript error if a new type is introduced but
// we haven't added a switch case for it.
const assertUnknown = (x: never) =>
console.log(`Unknown Pet type: ${(x as any).__typename}`);
switch (pet.__typename) {
case 'Cat':
console.log(`Cat meows ${pet.meowVolume}`);
break;
case 'Dog':
console.log(`Dog barks ${pet.barkVolume}`);
break;
default:
assertUnknown(pet);
console.log('Fallback behavior');
}

NOTE

When using graphql-code-generator, accessing known interface like Pet.name will trigger a TypeScript error. See

for more discussion.

// `when` must be used as an expression to require exhaustivity
val result = when (pet.__typename) {
"Cat" -> println("Cat meows ${pet.onCat?.meowVolume}")
"Dog" -> println("Dog barks ${pet.onDog?.barkVolume}")
else -> println("Unknown animal ${pet.__typename}")
}
switch pet.__typename {
case "Cat":
print("Cat meows \(pet.asCat?.meowVolume ?? 0)")
case "Dog":
print("Dog barks \(pet.asDog?.barkVolume ?? 0)")
default:
print("Unknown animal \(pet.__typename)")
}
Next
Home
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company