GraphQL Tour: Interfaces and Unions
GraphQL’s type system is the distinguishing feature in comparison to other protocols like REST, Falcor, and JSON Schema. Interfaces and Unions are two of the more nuanced aspects of the type system, but prove valuable in any non-trivial product. Let’s take a closer look at them and how they are used in GraphQL-JS.
Problem They Solve
Imagine we’re implementing some kind of search functionality in our GraphQL schema. Our query would probably start by looking like this:
So far so good — but now we need to describe the fields to return. If our server could only search against one type of data, like users, we could simply leverage a list of plain-old User types:
But let’s say our server is pretty smart and can search multiple types of data! It’ll still pick up users whose name contains “cat”, but also surface information about books and movies. These don’t have usernames or friends, but instead have bespoke fields like “director” and “author.” How should our query look now?
We could return individual fields per object-type:
But we lose the ability to control sorting on the server — for example, my friend named Catherine should appear earlier than the movie Catwoman. Wouldn’t it be great if we could say, “Hey GraphQL, if the search result is a User, then here’s the data I want — but if it’s a Movie, then send this data.”
Welcome to Interfaces and Unions! When the value for a field can be multiple types of data, you can use fragments with interfaces or unions to perform a sort of switch statement per-type:
The above query uses Inline Fragments to express the “if” conditions we want. Note that this query doesn’t have an explicit mention of whether or not searchResult returns a union or interface — in either case we’d use the same fragments, and our GraphQL tooling would detect whether the query was valid or not.
Implemented with Unions, our GraphQL server schema might look something like this:
Or using an Interface, the GraphQL schema may look more like this:
Notice how now each of the types has a searchPreviewText in order to implement the Searchable interface. Our original query would still work, but it means we can have this sort of concise query and still be valid:
Interface or Union?
Since we’ve just seen how the same query can be implemented with both a union and interface, you might be wondering, “How do I choose one or the other?”
The answer really depends on your application and your data model. Do the overlapping objects truly share a logical hierarchy, or are the in-common field names just coincidence?
A real-world example is the Facebook Graph API’s Profile type. Facebook’s data model thinks of Users, Pages, and Groups as all being “Profiles,” and accordingly they share many fields and API patterns. On the other hand, something like the Photo type isn’t meaningfully a “Profile,” despite having similar fields and URLs.
Usage with GraphQL-JS
We’ve talked about Interfaces and Unions in the abstract, but how do we implement these in code? The specifics will depend on your language and library, but we’ll use GraphQL-JS.
You can find the final working project on Github, and each commit matches a logical progression we’re going to follow. There’s some NPM/Babel boilerplate in the repo that you can copy, but we’ll skip those setup steps and focus on the GraphQL-JS bits.
Here’s our initial server code before we add any new types to the schema:
Pretty simple — our search field just returns whatever text we send it. But now we need to search some data and define the corresponding types. We’ll define some in-memory data described below, but you can imagine using an ORM or raw database queries to retrieve values asynchronously:
Next we define the corresponding types using pretty vanilla GraphQL-JS:
Time to hook up these types to our search field. We’ll start by defining a new Union type:
The types field should be self-explanatory — it’s an array of the possible types this union encompasses. But what about resolveType? Why do we need that? To shed some light, let’s first examine the changes we need to make to our schema’s search field:
Notice that the type for search is now a list of SearchableType, the new union type we defined. The search field has no direct idea about the User, Movie, or Book types — all it knows is that the resolve function will return something that is a valid SearchableType.
And that’s where resolveType comes into play. The GraphQL execution process will fetch the data from search.resolve, and then ask SearchableType, “Hey, I have this raw data, can you tell me what GraphQL Type it is?” Accordingly, the argument to resolveType is the exact same data we retrieved from our DATA source (or your database, external API, etc).
Our resolveType implementation uses fuzzy heuristics to determine the correct type, but that logic will change for every application. You might use the database table name, some form of type string in your objects, or a plethora of other techniques.
Now our earlier query with fragments works!
Let’s look at how to make this work with an Interface. Let’s change our union to a Searchable interface:
We’re now declaring that any type that conforms to Searchable must also have a string field called searchPreviewText. Notice that we did not specify how to resolve that field — each type must explicitly declare its own resolve function for that field.
We’re also re-using resolveType from earlier. The GraphQL execution process will ask the same sorts of questions to interfaces as unions (“hey, what type is this?”), but the difference with interfaces is that an interface may not know all the types that implement it.
Remember how we declared the types array for our union? Notice how that’s gone for the interface. An interface may be implemented by an unknown (and possibly huge) amount of types, so the GraphQL executor uses slightly different logic to determine the correct type.
In order to scan for the correct type, our schema needs to have its own types array now:
But everything else stays the same. This is a fairly recent change for GraphQL-JS, so read the 0.5 release notes if this is a surprise to you.
Finally, we need to update our object types to implement the interface correctly:
What if you need to share an interface across multiple projects? In fact, Relay intends for folks to do this with several interfaces. There’s no way Relay can implement resolveType for all the applications that could possibly consume it!
For those cases, GraphQL-JS offers an alternative to interface.resolveType. The GraphQL execution process will first traverse all of the types (defined by schema.types) and try to invoke isTypeOf if it is defined:
This makes it possible to define an interface in a library and shared it across other libraries or applications. I like to think of this as “horizontal scaling.” :)
That’s it for our whirlwind tour of interfaces and union. Next time you consider your data model, you won’t be able to un-see where you could use them. Check out the code for the examples in the post, and leave responses for any thoughts, questions, or issues you have!