Join us on Nov 7 for GraphQL Summit Virtual
Join API innovators, GraphQL experts, and AI trailblazers on Nov 7 at GraphQL Summit Virtual. Register for free →
With interfaces, we can encapsulate a set of fields that one or more object types have in common. Object types are said to "implement" an interface when they include all of the fields that the interface defines.
Interfaces exist as their own type in GraphQL, but unlike the Query
or object types, they don't return data directly. In order to be queried, an interface needs to come to life as an object type. This is because even though an interface defines fields, it's really more of a structure; it outlines the various attributes that an object type needs to have to qualify as an implementing type.
Let's see how we can unify the fields Fruit
and Vegetable
have in common with an interface called Produce
.
interface Produce {id: ID!name: String!quantity: Int!price: Int!nutrients: [String]vendor: Stall}type Fruit implements Produce {id: ID!name: String!quantity: Int!price: Int!vendor: Stallnutrients: [String]isSeedless: BooleanripenessIndicators: [String]}type Vegetable implements Produce {id: ID!name: String!quantity: Int!price: Int!vendor: Stallnutrients: [String]vegetableFamily: StringisPickled: Boolean}
The Produce
interface outlines the fields that both the Fruit
and Vegetable
types have in common. And Fruit
and Vegetable
include a special keyword, implements
, along with a reference to the Produce
interface.
Because they implement Produce
, Fruit
and Vegetable
are like members of the Produce
club.
This means that they each fulfill the requirements to be included, but they're also still able to define their own unique fields not shared by the other.
What happens when we decide that anything considered Produce
should also define a region
of origin?
We can add the new field to the Produce
interface, but we can't stop there; we have two object types that say they qualify to be described as "produce"! Now our interface has something they don't.
interface Produce {id: ID!name: String!quantity: Int!price: Int!nutrients: [String]vendor: Stallregion: String!}
Neither of our implementing types includes this region
field—as a result, neither Fruit
nor Vegetable
can be considered Produce
. They no longer fit the requirements!
This error is a good reminder for us to update both implementing type definitions to include the new region
field.
interface Produce {id: ID!name: String!quantity: Int!price: Int!nutrients: [String]vendor: Stallregion: String!}type Fruit implements Produce {id: ID!name: String!quantity: Int!price: Int!vendor: Stallregion: String!nutrients: [String]hasEdibleSeeds: BooleanripenessIndicators: [String]}type Vegetable implements Produce {id: ID!name: String!quantity: Int!price: Int!vendor: Stallregion: String!nutrients: [String]vegetableFamily: StringisPickled: Boolean}
It's easy to update one of the fields in the Produce
interface and forget to do the same in its implementing types. Fortunately, this logical structure we've put into place enforces that the implementing types abide by the rules the interface lays out. As a result, we're protected from forgetting fields in one place and including them in others; the interface is the agreement that Fruit
and Vegetable
need to adhere to for as long as they claim to implement Produce
.
An interface isn't just useful for creating a logical structure around similar fields between types; we want to use interfaces because of the flexibility they give us when querying!
As shown below, we can define a new field that returns the Produce
interface.
availableProduce: [Produce]
When we say that the availableProduce
field returns a list of Produce
types, what we really mean is that it returns a list of any type that implements the Produce
interface. This gives us the flexibility in our queries to pick between fruit, vegetable, or both!
By capturing the attributes that are common to our types, we've created a more generic placeholder for those different types. This means that anywhere that we have a field that should be able to return a Fruit
type, a Vegetable
type, or anything else that meets the criteria of the Produce
interface, we can set the field's return type to Produce
.
Let's take a closer look at how to query with interfaces in the next section.
Interfaces introduce a structure to help organize our types and the relationships between them. We can clearly see the elements that Fruit
and Vegetable
have in common, contained within Produce
.
interface Produce {id: ID!name: String!quantity: Int!price: Int!nutrients: [String]vendor: Stallregion: String!}type Fruit implements Produce {id: ID!name: String!quantity: Int!price: Int!nutrients: [String]vendor: Stallregion: String!isSeedless: BooleanripenessIndicators: [String]}type Vegetable implements Produce {id: ID!name: String!quantity: Int!price: Int!nutrients: [String]vendor: Stallregion: String!vegetableFamily: StringisPickled: Boolean}
We can now represent the category of attributes that Fruit
and Vegetable
share within our schema, and even use the Produce
interface in places where a field could return data for fruits AND vegetables!
To see this in action, let's take a look at an object type, Stall
.
type Stall {id: ID!name: String!stallNumber: String!availableProduce: [Produce]}
This availableProduce
field doesn't need to specify a particular object type that represents the kind of data it will return; instead, we can feel confident that when we query for a stall's availableProduce
, we'll get a list of items that fulfill the Produce
criteria. This means that we could get a list containing Fruit
types, Vegetable
types, and any other future types that implement the Produce
interface.
Use the Apollo Sandbox below to try this out. Under the Query
type, build a query for the mostPopularStall
at the marketplace. Query for the list of its availableProduce
, including the id
, name
, and price
of each item.
See the solution!
query GetMostPopularStallProduce {mostPopularStall {availableProduce {idnameprice}}}
When you clicked into the availableProduce
field on the Stall
type, you probably saw a lot of field information. First, we saw the fields that exist on the Produce
interface. This collection makes it easy to query for information that's common between all types belonging to the Produce
interface—like id
, name
, and price
!
The following two sections under "Implementations" detailed the interface's implementing types—namely, Fruit
and Vegetable
. We can see the fields that are unique to each of them listed in Sandbox as well.
So how do we include those unique fields as well? How can we use GraphQL to ask for additional details from the Fruit
and Vegetable
types in the same query?
When we're working with a particular object of data, we need to start by understanding which of the implementing types represents it. We can accomplish this with the __typename
field.
To understand what object types have been used to fulfill the data in a query, we can use a field that GraphQL provides for every single object type in our schema. This field, __typename
, is a meta-field that helps us understand the type of thing we're dealing with when we receive data. In other words, it identifies the object type that represents an object of data. (Note that __typename
starts with two underscores!)
If this sounds redundant, it's because most of the time we already know what kind of data we've just queried for. When we query for fruits
, for example, we know that we'll get back a list of objects that fit the Fruit
type!
type Query {# ...fruits: [Fruit!]!}
But sometimes we find ourselves working with a little more ambiguity. In the case of the Produce
interface, for example, the particular object of data we receive could be any one of the Produce
interface's implementing types. If we want to query for a particular implementing type's fields that are not part of the interface, we need to know what type an object of data belongs to.
interface Produce {id: ID!name: String!quantity: Int!price: Int!nutrients: [String]vendor: Stallregion: String!}
We can find out by using the __typename
field in our queries right away. Let's try the query again for availableProduce
, but this time we'll include the __typename
field for all the objects returned for availableProduce
.
In response, we'll see that each of the objects contained in the array now specifies the name of the object type that represents it.
{"data": {"mostPopularStall": {"availableProduce": [{"__typename": "Fruit","id": "F3","name": "pear","price": 79},{"__typename": "Fruit","id": "F2","name": "blueberry","price": 2},{"__typename": "Fruit","id": "F1","name": "banana","price": 44},{"__typename": "Vegetable","id": "V2","name": "celery","price": 150},{"__typename": "Vegetable","id": "V3","name": "sweet potato","price": 82}]}}}
Each object in our response is resolving either to a Fruit
or Vegetable
type, and this makes sense: they're the two implementing types of the Produce
interface. But we also know that Fruit
contains two fields that Vegetable
doesn't: hasEdibleSeeds
and ripenessIndicators
. And likewise, we can also query for data exclusive to Vegetable
with vegetableFamily
and isPickled
.
We'll see that if we try to add one of these fields alongside the others in our query, we encounter an error. Try running the provided query in the Sandbox below.
Notice the red squigglies? Sandbox tries to let us know that there's a problem with the
hasEdibleSeeds
field, even before we've run the query! Sandbox has introspected the service's schema, which means it understands which fields belong to theProduce
interface and which do not.
{"errors": [{"message": "cannot query field 'hasEdibleSeeds' on type '[Produce!]!'","extensions": {"type": "[Produce!]!","field": "hasEdibleSeeds","code": "INVALID_FIELD"}}]}
Recall that availableProduce
should return a list of Produce
objects; each object resolves with data that can be represented by one of the implementing types, Fruit
or Vegetable
. An error occurs when we try to ask for hasEdibleSeeds
here because it exists exclusively on the Fruit
type. It's invalid to include this field when we're potentially querying for Vegetable
data as well.
This doesn't mean that our query has to leave out any fields exclusive either to Fruit
or Vegetable
—we simply need to specify additional syntax to help GraphQL understand which implementing type we'd like to apply the field to. We can do this using fragments.
Fragments let us define a set of query fields that we can save and use in more than one place. They're a good tool to use when we want to simplify the appearance of our queries or cut down on repeated syntax.
Fragments should be defined alongside the query using them, whether that's in an Apollo Sandbox or in the frontend code of an application. (We'll refer to a fragment within the body of the query, so wherever we define it needs to be accessible!)
Named fragments are defined using the fragment
keyword, followed by a name we'll use to refer to the fragment anywhere we'd like to use it. Finally, we specify the object type the set of fields in the fragment applies to using on
and the object type's name. Within the body of the fragment, we can define exactly the fields we'd normally place within the query.
fragment stallProductFields on Stall {nameavailableProduce {nameprice}}
When included in queries, fragments are applied using the spread operator (...
), followed by the fragment name.
query GetMarketStallsAndFeatured {featuredStall {...stallProductFields}allMarketStalls {...stallProductFields}}
This has the same effect as writing out all of the fields individually!
Fragments can also be applied conditionally—that is, when we want to ask for certain data only if a particular object type is encountered.
This makes fragments ideal when querying fields specific to an interface's implementing types. We can define a section of the query that is applicable exclusively to one type or another, letting us avoid the problem of asking for a field that doesn't exist on other object types.
To put this into practice, we'll use a type of fragment called an inline fragment. It's a fragment that we define right within the query, and just as with named fragments, we begin with the spread operator: ...
. Next, we use the on
keyword, followed by the name of the object type that we're targeting. Finally, we open up a pair of curly braces.
query GetMostPopularStallProduce {mostPopularStall {availableProduce {__typenameidnameprice... on Fruit {}}}}
Now anything that we specify within the curly braces following Fruit
will be queried only from objects whose __typename
field has Fruit
as its value. This means that we can include any of the fields that only the Fruit
type has, and they'll never be applied to objects with a __typename
of Vegetable
.
query GetMostPopularStallProduce {mostPopularStall {availableProduce {__typenameidnameprice... on Fruit {hasEdibleSeeds}}}}
We can do the same for the unique fields on the Vegetable
type, defining a separate inline fragment below the first and changing the object type name. Here we can specify anything that we'd like to query from vegetable-specific data when GraphQL encounters data objects containing a __typename
field equal to Vegetable
.
query GetMostPopularStallProduce {mostPopularStall {availableProduce {__typenameidnameprice... on Fruit {hasEdibleSeeds}... on Vegetable {vegetableFamily}}}}
Try it out!
Fragments let us fully utilize the power of the graph, by specifying conditionally which types should be queried for particular fields. We can benefit from the organizational power of the Produce
interface, without limiting the way we query data with separate fields for Fruit
and Vegetable
. And best of all, we can use just one query to ask for precisely the data we need—right down to the last detail of each fruit or vegetable!
Interfaces are great for grouping commonality between object types. But we might want a way to query distinctly for this or that, without explicitly declaring shared fields between objects. Let's see how we can represent this using unions.
When we want to give a field more flexibility to return one object type or another without any intersection, we can use a union. With a union, we can define a new type that lists out the different possible object types it can resolve to. Unlike an interface, a union doesn't enforce a set of common fields all of the object types need to implement. This gives us the ability to define fields that can return this, or that, or another thing, depending on the object we're requesting data from.
Imagine a union like a box containing some number of different, unrelated items. These items don't need to have anything in common, but we access them all through the same box. Depending on the item we pull out, we can then ask for more specific data about its particular object type.
Let's look at an example of a union
type called Offer
.
union Offer = Discount | Coupon | ComplimentaryItem | Refund
In this example, we can see that a reference to Offer
will give us a Discount
, or a Coupon
, or a ComplimentaryItem
, or even a Refund
.
Let's think about why these object types are separate. Each stall in a market might configure different special offers for the items they sell. They might distribute and track coupon codes, or set up discounts for first-time customers or for combination purchases. Stalls might even want to occasionally include samples, or a complimentary item, with every purchase. They will likely also encounter times when they need to issue partial or even full refunds on previous purchases.
type Discount {id: ID!code: String!percent: Float!description: Stringqualifications: [String]orders: [Order]}type Coupon {id: ID!code: String!description: Stringamount: Float!orders: [Order]}type ComplimentaryItem {id: ID!name: String!limitPerCustomer: Intquantity: Int!orders: [Order]}type Refund {id: ID!order: Order!isPartialAmount: Boolean!refundAmount: Float!}
We don't want to limit the types of offers a stall could set up—that's part of their individual business strategy!—so a union lets us create a place for this information to live that can be customized on an order-by-order basis. In the case of the Offer
union, all four of these object types could reasonably be the type of offer on an order, but the data we care about for each could differ enormously.
Let's look at how to use reference a union type in our schema.
type Order {id: ID!vendor: Stall!items: [OrderItem!]!orderOffer: Offer}type Stall {id: ID!name: String!stallNumber: String!availableProduce: [Produce!]!orders: [Order!]}
An object type called Order
could track the details of each order placed at a particular stall. In addition to setting an id
, we'd track the vendor
who sold the items, and the items
that were purchased. Each order would also keep track of the offer applied, if necessary.
Notice in the example that a
Stall
type has a list ofOrder
types. From the other side, we can access more information about a particularStall
anOrder
is assigned to via theOrder
type'svendor
field. This two-way relationship lets us traverse fromStall
toOrder
, or vice versa!
We can even use the union type within a list, which would enable any given Order
to apply more than one Offer
—good news for customers!
type Order {id: ID!vendor: Stall!items: [OrderItem!]!orderOffers: [Offer]}type Stall {id: ID!name: String!stallNumber: String!availableProduce: [Produce!]!orders: [Order!]}
We've mentioned that one of the biggest differences between an interface and a union is that a union does not enforce that all of its object types implement a subset of common fields. As a result, when we query a field that returns a union type, we have to be more specific about the fields we want and which of the object types they can be found on. Let's look at an example, using the Order
type above.
We might want to review the number of orders placed at this week's featured stall. Using a featuredStall
field on our Query
type, we could build out a query to request information about the items contained in each order.
Note that the
items
field on theOrder
type returns a list of typeOrderItem
.OrderItem
has two fields,quantity
to represent the number of an item included in a transaction, and theitem
itself, which resolves to aProduce
object.Produce
is an interface that can resolve to one of two implementing types:Fruit
andVegetable
.
It might also be important to find out which of a stall's offers are most commonly used in its orders. This could help business owners make decisions about what's working, what's not, and where they have opportunities to maximize their sales by improving offers on the items people enjoy the most.
type Order {id: ID!vendor: Stall!items: [Produce!]!orderOffers: [Offer]}
With the orderOffers
field, we can ask for these details. But because there's no guaranteed common set of fields between the possible object types in the Offer
union, we need to use inline fragments to target each type's fields individually.
query GetFeaturedStallOrders {featuredStall {orders {items {quantityitem {name}}orderOffers {... on Discount {percent}... on Coupon {amount}... on ComplimentaryItem {limitPerCustomer}... on Refund {isPartialAmount}}}}}
This query returns an array of data we can inspect further to understand how frequently each type of Offer
occurs, along with additional information about each type.
Try out this query in the Sandbox below, but make one adjustment: for each of the Offer
types, return the __typename
field as well. Let's see how the Offer
type resolves to Coupon
, Discount
, ComplimentaryItem
, or Refund
for each of the featured stall's orders
query GetFeaturedStallOrderOffers {featuredStall {orders {idorderOffers {... on Discount {__typenamepercent}... on Coupon {__typenameamount}... on ComplimentaryItem {__typenamelimitPerCustomer}... on Refund {__typenameisPartialAmount}}}}}
We can use interfaces and unions in our schema to better reflect the relationships between types and fields.
Interfaces allow us to define an object with a set of fields that multiple implementing object types have in common. When a field returns an interface, we can query any of the fields shared by its implementing types; or we can target fields exclusive to just one of the types.
With unions, we can permit a field to return a type from a list of different object types even if they do not implement any of the same fields. With both interfaces and unions, we can employ fragments to directly specify the fields we want to include from a particular object type. These tools help us build flexibility into our graph, and result in more expressive queries that can target exactly the data we need from our GraphQL service.
Up Next: ArgumentsAbout
GraphQL.com is maintained by the Apollo team. Our goal is to give developers and technical leaders the tools they need to understand and adopt GraphQL.
GraphQL.com 2024