Overview
In Airlock, a user can be either a host or a guest. These two user types share some common attributes. For example, they both have names and profile pictures. They also have a few attributes that are specific to their role: only hosts have a profile bio and listings, and only guests have bookings.
To implement this business logic into our GraphQL schema, we can use an interface.
In this lesson, we will:
- Learn how to implement interface types in a GraphQL schema
- Learn how to resolve interface types
What is an interface?
An interface is an abstract type that defines a common set of fields that any number of object types can then include.
Interfaces are often used to represent an important relationship among different types with some shared behavior. For example, the Airlock schema defines separate types for Host
and Guest
, but the shared attributes between these two types are captured in a User
interface.
When an object type uses an interface, it's called an implementing object type. We also say that the type "implements" the interface. For example, the Host
type implements the User
interface.
An interface specifies a contract that its implementing types must follow. In other words, an implementing object type must include all the fields defined on that interface. For this reason, we recommend creating meaningful interfaces, to avoid unnecessary schema maintenance as your graph evolves.
An implementing object type can also define any number of additional fields that aren't part of the interface.
Defining an interface
In a GraphQL schema, we define an interface using the interface
keyword, then the name of the interface. After the curly braces, we define the fields as we've done before in other schemas using the schema definition language.
Here's what Airlock's User
interface looks like:
"Represents an Airlock user's common properties"interface User {id: ID!"The user's first and last name"name: String!"The user's profile photo URL"profilePicture: String!}
Any type that implements our User
interface must define these exact fields with these exact return types (including nullability). It can also define any number of other fields. (More on that in a moment...)
Implementing the interface
Once an interface has been defined, it can be implemented by other types in the schema.
To define an implementing object type, we start by writing the keyword type
, followed by the name of the type. Then, we add the keyword implements
, followed by the name of the interface. Next, we add all the fields defined by the interface to the type definition.
Here's how Airlock defines the Host
and Guest
types, which both implement the User
interface from the previous section:
type Host implements User {id: ID!"The user's first and last name"name: String!"The user's profile photo URL"profilePicture: String!"The host's profile bio description, will be shown in the listing"profileDescription: String!}type Guest implements User {id: ID!"The user's first and last name"name: String!"The user's profile photo URL"profilePicture: String!"The reservations guest has"bookings: [Booking]!}
Note that the Host
and Guest
types both also have additional types that aren't part of the User
interface (Host.profileDescription
and Guest.bookings
).
Returning an interface
An interface can also be used in the schema as a return type. In the Airlock schema, the Review.author
field is a perfect example of a field that returns a User
type. A review author can be any user, whether a host or a guest.
type Review {# ... other fields"User that wrote the review"author: User!}
Note: The Query.me
field is also a good example. We'll take a closer look at that field in the next lesson.
Resolving an interface
A field that returns an interface can return any object type that implements that interface. But this raises a question: for a given operation, how do we know which implementing type the field is returning?
For example, in Airlock the Review.author
field might return a Host
object, or it might return a Guest
object. How would we know whether the object that's returned is a host or a guest?
To handle this, we need to define a special resolver function called __resolveType
.
The __resolveType
resolver
The __resolveType
function is responsible for determining which implementing object type is being returned. It returns a string with the name of the corresponding object type.
For example, the User
interface's __resolveType
function should return either "Host" or "Guest" because those are the two implementing object types defined in the schema.
Unlike our other resolver functions, __resolveType
takes in three optional arguments: obj
, context
and info
.
- The first argument,
obj
, is the object returned by the resolver for the field returning the interface. - The last two arguments remain the same as we covered in Lift-off II.
__resolveType(obj, context, info) {// logic to determine which type to return goes here}
The logic for determining which object type is being returned depends on the application! In Airlock's case, each user in the accounts
database has a role
attribute that is set to either "Host" or "Guest". This is perfect to use, because the two types that implement this interface are also Host
or Guest
.
User: {__resolveType(user) {return user.role; // returns "Host" or "Guest"},},
Note: Because we don't use either of the last two arguments (context
or info
), we omitted them from the function call. We also renamed the first argument obj
to user
to better clarify what the object is.
Test it in GraphOS Studio
Let's use GraphOS Studio to try out a query that resolves an interface.
In a web browser, open
http://localhost:4000
in GraphOS Studio Sandbox.In the Operations tab, let's start building up our query. We'll start by adding the
featuredListings
field and itsreviews
subfield. From here, we can add theauthor
field, which we know resolves to theUser
interface.query GetFeaturedListings {featuredListings {reviews {author {# TODO!}}}}
When we add the author
field to our query, notice how the Documentation panel updates to show us the possible Implementations for this interface.
Here, we can see the shared fields encapsulated in the User
interface, alongside those that are specific either to Host
or Guest
.
For now, let's just look at the fields that both implementing types share in the User
interface: id
, name
and profilePicture
.
Add the
id
,name
, andprofilePicture
fields to the query in GraphOS Studio. Here's what the final query should look like:query GetFeaturedListings {featuredListings {reviews {author {idnameprofilePicture}}}}When we run the query, we see our data returning. Fantastic! The response should look something like the object below.
See it in the Airlock codebase
Check out the interfaces defined in the Airlock codebase. You can find them in the server/schema.graphql
file.
Practice
Use the following schema to complete the code challenge below:
type Query {availableBooks: [Book]borrowedBooks(userId: ID!): [Book]}interface Book {isbn: ID!title: String!genre: String!}type PictureBook implements Book {isbn: ID!title: String!genre: String!numberOfPictures: IntisInColor: Boolean}type YoungAdultNovel implements Book {isbn: ID!title: String!genre: String!wordCount: IntnumberOfChapters: Int}
Write the __resolveType
resolver for the Book
interface. To determine the type of Book
, you can use the book's hasPictures
property. PictureBook
types have this property set to true
, while YoungAdultNovel
types do not.
Key takeaways
- An interface defines a common set of fields that any number of object types must include.
- We recommend creating meaningful interfaces to avoid unnecessary schema maintenance as the graph evolves.
Up next
So far, we've learned how to implement an interface and resolve the fields that are shared between all implementing types.
In the next lesson, we'll see how to query fields that belong exclusively to one implementing type and not another. To do that, we'll need to learn about query fragments.