GraphQL.com

LearnTutorialsCommunityEventsFAQs  

Table of contents

  • What is GraphQL?
  • The Query
  • Types
  • Schema
  • Interfaces & UnionsIntroducing InterfacesQuerying with interfaces__typenameFragmentsIntroducing unionsQuerying with unionsSummary
  • Arguments
  • Mutations

What is GraphOS?The GraphQL developer platform

Interfaces & Unions

Object types can be used to represent concrete objects in GraphQL. But in many cases, we'll find that some of our object types have many of the same fields in common. We might track the same kinds of data about closely related object types - like Fruit and Vegetable - because while they appear distinct enough to stand on their own, they share innate similarities.

A pear representing the Fruit type, and an onion representing the Vegetable type. Though they're separate types, they have six fields in common!

In the type definitions below, both Fruit and Vegetable have several fields in common: id, name, quantity, price, vendor, and nutrients. For example, we can expect the same type of data when we query Fruit.price as when we query Vegetable.price.

type Fruit {
id: ID!
name: String!
quantity: Int!
price: Int!
nutrients: [String]
vendor: Stall!
ripenessIndicators: [String]
hasEdibleSeeds: Boolean
}
type Vegetable {
id: ID!
name: String!
quantity: Int!
price: Int!
vendor: Stall!
nutrients: [String]
vegetableFamily: String
isPickled: Boolean
}

Note: We've removed the field descriptions from each of the types above to keep the schema as legible as possible. In practice, be sure to include descriptions for your fields - they're a great place for crucial information about how to interpret the field's data!

But we've kept these definitions separate because Fruit and Vegetable each define two unique fields the other type doesn't know about. This means that if we want information for fruits and vegetables, we need to query each type separately.

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

Any object types that refer to either Fruit or Vegetable, such as the Stall type shown below, would need to have each type served by different fields.

type Stall {
id: ID!
name: String!
stallNumber: String!
availableFruits: [Fruit!]!
availableVegetables: [Vegetable!]!
}

Remember the syntax when a Non-Null type is combined with a List type? The availableFruits and availableVegetables fields return different kinds of data, but their rules are the same: the list of data they get back can be empty, but the list itself can't be null. And if the list does contain items, they can't be null either!

Try it out! The Sandbox below has a marketStalls field on the Query type. Use this field to query for all availableFruits and availableVegetables from each of the market's stalls.

But what if we just want one list, containing all of the data returned by the availableFruits and availableVegetables fields? After all, it would be useful to use just one query to take stock of all the fruits and vegetables that a stall sells.

At first glance, we might want to combine the Fruit and Vegetable object types. This would let us share the fields they have in common, but there's one big problem: this new type would include some fields relevant exclusively to fruits, and other fields relevant exclusively to vegetables.

type FruitOrVegetable {
id: ID!
name: String!
quantity: Int!
price: Int!
nutrients: [String]
isSeedless: Boolean # Available only for fruits
ripenessIndicators: [String] # Available only for fruits
vendor: Stall!
vegetableFamily: String # Available only for vegetables
isPickled: Boolean # Available only for vegetables
}

To make use of this FruitOrVegetable type, our Stall type might look like this:

type Stall {
# ...
availableFruitsOrVegetables: [FruitOrVegetable]
}

The query in the Sandbox below asks for the name and list of fruits or vegetables from each stall at the market.

Everything works, right? Well, that's because we've queried for fields that the Fruit and Vegetable type have in common. Try out the query below - we've added a few fields that are specific to one type or another, but not both.

When we run this query, we'll see that even our fruit objects have a vegetableFamily property - but its value is null! The same is true of our vegetable objects: we see a hasEdibleSeeds property even though vegetables, by definition, contain no seeds.

But why does this matter? We could just accept the null values for the fields that don't apply for each type, right?

Well, it might seem like a small problem now when we're talking about fruits and vegetables. But this approach creates more work for anyone using the graph - and it doesn't actually represent the reality of our data!

Object types allow us to draw clear boundaries around a type of thing in our service, and its different details we might want to query for. But if we cram two types together, unique attributes included, someone could reasonably ask for a fruit's vegetableFamily, or a vegetable's list of ripenessIndicators - and wonder why they don't get the data they're expecting!

When we create a type that includes irrelevant or non-existent data, we neglect one of the core strengths of GraphQL. The single source of truth we get from a schema is only as good as our ability to rely upon the capabilities it describes. In the FruitOrVegetable type example, we have several fields that are irrelevant for much of the data this type should represent.

So if we have two or more types that each define a common set of fields, alongside some fields all their own, what's the best way to organize them? How can we represent the similarity between types and preserve their object type boundaries simultaneously? To see how we can use just one field to query both Fruit and Vegetable types, let's talk about interfaces!

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.

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 revisit the problem of the FruitOrVegetable type, and revise it 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
}

What's different? We have the addition of the Produce interface, which outlines the fields that both the Fruit and Vegetable types have in common. But now we've updated Fruit and Vegetable with a special keyword, implements, along with a reference to the Produce interface.

By implementing the Produce interface, Fruit and Vegetable are like members of the Produce club. They each follow the requirements to qualify 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: 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 very easy to update one of the fields in the Produce interface and forget to do the same in the 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.

So how does this change the way we query for the Fruit and Vegetable object types associated with a particular Stall object?

Well - right now, it doesn't. Take a look at the Sandbox below. The Stall type still needs to query for a list of availableFruits and availableVegetables through different fields.

We're not seeing any difference because we haven't actually used our interface yet! 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!

Just as we've used availableFruits and availableVegetables for the Fruit and Vegetable object types, we can create a new field that returns a list of Produce objects!

availableProduce: [Produce]

In our earlier example, we talked about wanting to query data about fruits and data about vegetables with a single field. This is exactly the purpose that an interface satisfies, because it lets us bring this flexibility into 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 simply 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.

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 to us 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.

Take 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 around the world all of the tools they need to understand and adopt GraphQL.


GraphQL.com 2023