Experience GraphQL Summit 2024: Watch On-demand →

Experience GraphQL Summit 2024: over 45+ technical sessions, real-world success stories, next-gen product demos and more. Watch On-demand →

GraphQL.com

LearnTutorialsCommunityEvents

Table of contents

    GRAPHQL BASICS

  • What is GraphQL?
  • The Query
  • Introducing Types
  • Scalars, Objects, and Lists
  • Nullability
  • Querying between Types
  • Schema
  • Enums
  • Interfaces & UnionsIntroducing interfacesInterface as a return typeQuerying with interfaces__typenameFragmentsIntroducing unionsQuerying with unionsSummary
  • Arguments
  • Mutations
  • FAQ


Try GraphOS  
The GraphQL developer platform

Interfaces & Unions

Introducing interfaces

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.

A pear represents the Fruit type, and an onion represents the Vegetable
type, but they have six fields (such as id, name, quantity, price) in
common.

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.

An image showing a blueprint representing "Produce", which can be used to
represent a lime just as well as a
potato.

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: Stall
nutrients: [String]
isSeedless: Boolean
ripenessIndicators: [String]
}
type Vegetable implements Produce {
id: ID!
name: String!
quantity: Int!
price: Int!
vendor: Stall
nutrients: [String]
vegetableFamily: String
isPickled: 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.

A potato and a lime, both with a sticker that identifies each as a member of
Produce.

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.

A pear represents the Fruit type, and an onion represents the Vegetable
type. They have six fields in common, but each also defines two of its own
unique fields that the other does
not.

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: Stall
region: 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: Stall
region: String!
}
type Fruit implements Produce {
id: ID!
name: String!
quantity: Int!
price: Int!
vendor: Stall
region: String!
nutrients: [String]
hasEdibleSeeds: Boolean
ripenessIndicators: [String]
}
type Vegetable implements Produce {
id: ID!
name: String!
quantity: Int!
price: Int!
vendor: Stall
region: String!
nutrients: [String]
vegetableFamily: String
isPickled: 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.

Interface as a return type

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!

Both the Fruit and Vegetable type derive common fields from the Produce
interface, but each defines unique fields the other does
not.

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.

Querying with interfaces

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: Stall
region: String!
}
type Fruit implements Produce {
id: ID!
name: String!
quantity: Int!
price: Int!
nutrients: [String]
vendor: Stall
region: String!
isSeedless: Boolean
ripenessIndicators: [String]
}
type Vegetable implements Produce {
id: ID!
name: String!
quantity: Int!
price: Int!
nutrients: [String]
vendor: Stall
region: String!
vegetableFamily: String
isPickled: 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 {
id
name
price
}
}
}

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.

__typename

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: Stall
region: 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 the Produce 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

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 {
name
availableProduce {
name
price
}
}

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 {
__typename
id
name
price
... 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 {
__typename
id
name
price
... 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 {
__typename
id
name
price
... 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.

Introducing 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: String
qualifications: [String]
orders: [Order]
}
type Coupon {
id: ID!
code: String!
description: String
amount: Float!
orders: [Order]
}
type ComplimentaryItem {
id: ID!
name: String!
limitPerCustomer: Int
quantity: 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 of Order types. From the other side, we can access more information about a particular Stall an Order is assigned to via the Order type's vendor field. This two-way relationship lets us traverse from Stall to Order, 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!]
}

Querying with unions

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 the Order type returns a list of type OrderItem. OrderItem has two fields, quantity to represent the number of an item included in a transaction, and the item itself, which resolves to a Produce object. Produce is an interface that can resolve to one of two implementing types: Fruit and Vegetable.

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 {
quantity
item {
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 {
id
orderOffers {
... on Discount {
__typename
percent
}
... on Coupon {
__typename
amount
}
... on ComplimentaryItem {
__typename
limitPerCustomer
}
... on Refund {
__typename
isPartialAmount
}
}
}
}
}

Summary

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: Arguments  

About

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