All About GraphQL Abstract Types

May 05, 2020 | 10 min read

This post is a snippet from Production Ready GraphQL. It is part of an update I’m working on for the schema design chapter in version 1.3. I decided to release it publicly as well because this side of abstract types is not talked about very much, and the subtle differences between interfaces and unions are not always easy to grasp. Hope you enjoy!

GraphQL Schema designers have in their toolbox two different abstract types they can choose from: Interfaces and Unions. While both of them allow us potentially return different concrete types for a single field, they work quite differently.

Interface types are all about contracts. Object Types may implement interfaces, which is a way to say that this object satisfies the interface’s contract. Take for example the Closable interface in GitHub’s GraphQL API:

Interfaces are great because then clients don’t need to know the exact type when dealing with a Closable*. *They can simply expect types that implement the Closable interface to answer contract, which in this case is having a closed and a closedAt field. In practice, queries that query on interfaces looks like this:

So interface types are best used when you expect the different concrete types to share similar behaviors and you can find a common contract between them. But what about when a field could return one of many different types and when you can’t really find a contract between them? This is where Union types come in.

Union types are different in the sense that types that are part of a union are not aware of that fact. While types that implement interface explicitly join them (through the implements keyword in the SDL), union types are local to a specific field. For example in the example above, the BlogPost type itself is not aware it’s part of the SearchResult union.

Unions are queried similarly to interfaces, but they have one major difference: You can’t select any common fields since the possible types within a union don’t technically have any behavior in common. That means that even if all the types on the union share fields, you have to repeat them on every concrete type fragment

Notice how even if `createdAt` is a field on all the types it still has to be queried at the three locationsNotice how even if createdAt is a field on all the types it still has to be queried at the three locations

Both interfaces and union types include, per the GraphQL spec, a __typename field. This lets the client query for which concrete type it is dealing with. As you can see, interfaces and unions are incredibly similar. They are so similar that it could be a question whether they could be removed in favor of **empty interfaces**.

API Evolution

Abstract types like interfaces and unions can possibly be a great help to evolving our schemas. For example, take a field called actor , which represents the entity behind a certain Event . If initially the only kind of actor in our system is a User , we would probably design our schema this way:

Over time, when new actor types emerge like Robot and Organization maybe, this schema is now pretty broken. An abstract type, like a Union, might seem like it would’ve saved us here:

It’s true: Imagine clients are now querying that entity union field:

If we were to add a 4th possible type to the union, the query above would still be technically valid, in GraphQL terms. However, if this 4th type or condition needed to be handled for logic on the client side to be valid, it would likely break this client. What if this was banking logic around transactions for example?

The client here looks at a transaction, which can be possibly an Authorize or a Charge when it first implements it. Later on, the server adds a third possibility, a Refund . This is such an important logic change to an application dealing with money that even though the above query remains valid, it would compute things in the wrong way, because it ignores refunds! This is why using a union type does not necessarily help with evolution more than anything else, especially if we expect new cases or possible types to change the behavior of a client.

Typical union types, for example in some programming languages, can let consumers of the API do exhaustive case checks against the types in a union. This means that a switch statement for example would fail to compile if you did not explicitly handle all union members. This offers such a great developer experience, since it will never let us make mistakes such as forgetting to handle a new union member. Unfortunately, we often can’t afford this luxury when it comes to web APIs. Especially with GraphQL’s tendency to go for a continuous evolution approach, gracefully adding new enum values, union members and interface implementations is often very hard. Note that this would not necessarily be the case if we versioned our schema and cut a new version every time a new case is added, but this comes with its own disadvantages.

All that to say that unless you’re versioning your GraphQL schema with fine grained versions, union types are much less powerful than union types in some programming languages like OCaml, which have powerful type systems that allows for these checks.

Interfaces suffer from similar pains, but at least we can rely on the contract they define. Client may not be able to handle new concrete types, but if they rely mostly on the common contract, the client is more likely to evolve well, unlike unions.

Porque no los dos

There are scenarios where we can harness the power of both unions and interfaces. Errors are one of them. A common way to represent error states in GraphQL is by using a union type:

But this design, as we covered earlier, makes it really hard for clients to handle new error types. The union here is still helpful though, it lets clients discover very quickly see the full possible set of errors here. The other thing is that design doesn’t let the server return more than a single error (that’s not necessarily bad, but worth noting). We can move things around and achieve something potentially much more future proof:

Not bad but we lost the ability to tell quickly what kind of errors can happen there. Many things may implement Error but that doesn't mean they may appear in the createPost mutation. One way around that could be to create a specific error interface per mutation, but it oddly feels like what a union should be doing.

A bit funky right? Now we’ve got a union that clearly conveys what errors may happen, but these errors also implement the Error interface. This lets us query under the Error fragment to select the common fields, even under a field that returns a union. We can always select for interfaces, even if the field doesn’t return an interface directly!

Note that I don’t necessarily dislike the interface-only solution either. But the union-only approach doesn’t feel as robust to me, because of the lack of a contract between errors. In this last example, we’d probably want to make sure that any new error added to our result unions implement Error . We can achieve that with something like https://github.com/cjoudrey/graphql-schema-linter.

So… how should you incorporate abstract types in your schema in general? I think we can follow a few best practices here that will probably guide us towards an API that will evolve in a better way and ultimately be more usable as well.

Prefer interfaces over unions as long as the types share behavior / fields

As you can see, interfaces are a lot more fun to evolve than union types, especially when you expect the set of concrete types to change over time. When possible, using an interface will probably be the best choice over time. Using a union with possible types backed by an interface might be a great choice as well.

Prefer small, focused interfaces to super generic interfaces

The more specific our interfaces are, the more the client knows what to be looking for when new concrete types get added. Usually, choosing an interface that makes the most sense in a field context might be a better choice.

In this case, it lets users select the Sponsor specific fields when querying. Note that they could also use ... on Actor when needing more generic fields. But they can be assured that totalDonations and sponsorShips will always be present no matter what kind of new Sponsor appears. If let’s say, something completely different from an Actor were to appear, it only has to implement these two fields, rather than being a full Actor .

What about mutations?

Input unions are coming! This will allow so many different scenarios like nicer batch mutations for example. At the moment though, it’s kind of hard to express how interfaces and union types relate to the input of mutations. In a typical programming language, we might expect functions to receive a particular interface, or union type as input. In GraphQL, we don’t have that mechanism just yet.

One thing we can do is at the very least annotate mutations with what kind of types they deal with. This is most common with mutations that accept an ID 👇

What if we used directives to let both our schema developers and client developers what can be accepted as input here?

This gives us two really cool things:

  1. Our documentation/reference can now consume this to document what types the mutation may accept.
  2. When new types implement closable, we can verify through a linter / CI check that the closeClosable has been updated with code that can close this new object. The @possibleTypes directive can then be updated.

Can we improve GraphQL evolution somehow?

I wish API providers could help clients handle new concrete types gracefully. It’d be super nice if clients could select concrete types ahead of time.

Of course, that’s not a simple change since this is today an invalid query ☝️Maybe a special directive on unions and interfaces similar to deprecated:

@upcomingChange(change: ConcreteTypeAddition, type: "Discussion")

Conclusion

To finish up this post, let’s sprinkle some nuance on all this. Not all interfaces or unions are critical in the sense that an application would be broken by the addition of a new type. (But certain are! Errors come to mind) Abstract types are still a very powerful tool for a more expressive schema and one that can adapt to changes more easily. Still, developers who expect union types to be as powerful as in certain programming languages might be tempted to use them a little too much in GraphQL API (used in a web context).

One last thing: Recently interface types became even more expressive thanks to a new RFC that allows interfaces to implement other interfaces, you can check it out right here https://dev.to/mikemarcacci/intermediate-interfaces-generic-utility-types-in-graphql-50e8

Keep that in mind next time you design a GraphQL schema! 💚

Marc

If you've enjoyed this post, you might like the Production Ready GraphQL book, which I have just released!

Thanks for reading 💚

Sign up for my newsletter

Stay up to date when I release courses, posts, and anything related to GraphQL

No spam, just great GraphQL content!

© 2020 MYUL Digital, Inc. All rights reserved.