Screen_Shot_2014-05-30_at_1.37.33_PM

Salsify Engineering Blog

Polymorphism in GraphQL

Posted by Dan Freeman

Oct 9, 2020 11:28:58 AM

Or: How I Learned to Stop Worrying and Love __typename

Background

In APIs (and in domain modeling in general) it's common to want to represent fields that may point to one (or more) of several different types of object, a.k.a. polymorphism. In GraphQL's type system, this can be accomplished with either a union or an interface, depending on whether the objects in question are expected to have anything in common.

What's not always obvious to newcomers to GraphQL, though, is how to best handle that data on the receiving end, when you need to tell what concrete type of object you're dealing with.

A Galaxy Far, Far Away

The Star Wars schema is the de facto standard for discussing GraphQL schemas, so we'll go with an abridged variant of that here. I'll assume a basic understanding of the GraphQL type system, but detail the specifics relevant to polymorphism below. If you're already familiar with unions and interfaces in GraphQL, feel free to skip on down to Handling Polymorphic Responses.

The three primary types we're concerned with are Starship, Human and Droid.

type Starship {
  id: ID!
  name: String!
  length: Float!
}

type Human implements Character {
  id: ID!
  name: String!
  friends: [Character!]!
  starships: [Starship!]!
  totalCredits: Int!
}

type Droid implements Character {
  id: ID!
  name: String!
  friends: [Character!]!
  primaryFunction: String!
}

Interfaces

Interfaces represent commonalities shared across different concrete types. You may have noticed in the snippet above a reference to a Character type—this represents the set of things that Humans and Droids have in common.

interface Character {
  id: ID!
  name: String!
  friends: [Character!]!
}

By declaring that Human and Droid both implement Character, we tell consumers of our API that they should be prepared for objects of either of those two types to appear anywhere that a field has type Character. Interfaces are an example of subtype polymorphism, where we say that there may be more to know about any specific concrete object, but it will always have at least the fields the interface promises.

When querying against a field that returns an interface, all the fields of that interface are fair game to select directly:

query JediCharacters {
  characters(episode: JEDI) {
    id
    name
    friends { id }
  }
}

However, it's illegal to directly select any field that isn't part of the interface, even if one or more of the implementing types does have a field with that name.

query JediCharacters {
  characters(episode: JEDI) {
    id
    totalCredits # 💥 Not allowed!
  }
}

So how would I select the totalCredits field for any objects in the result set that were Humans? The answer is fragments, which brings us to the other variety of output polymorphism in GraphQL: unions!

Unions

Unions are effectively a bag of unrelated types, with no promises made about whether there's any commonality between them.

union SearchResult = Human | Droid | Starship

Unlike interfaces, which types must declare themselves to be members of with an implements clause, unions are an example of ad hoc polymorphism, where the collection of possible types a union might be is declared on the spot.

To select fields on a union type, you have to use an inline fragment (or a named one) to declare which member of the union you're trying to match against:

query SearchFor($query: String!) {
  search(text: $query) {
    ... on Human {
      name
      height
    }
    ... on Droid {
      name
      primaryFunction
    }
    ... on Starship {
      name
      length
    }
  }
}

This is also how you can select type-specific fields when querying against an interface.

Note that even though all three members of the SearchResult union have a name field, you still can't request that field outside of a fragment targeting a particular member type. This helps with API evolution, as it avoids accidentally breaking queries as new members are added to a union.

Handling Polymorphic Responses

All of the above tells us how to get our hands on fields of polymorphic values, but not how we can tell what to do with them. Depending on the fields I request, I might be able to inspect the object itself to figure out what type of thing it is, but what if all I care about from my search results is the name and id, and what kind of thing it is?

query SearchFor($query: String!) {
  search(text: $query) {
    ... on Character { id name }
    ... on Starship { id name }
  }
}

It's A Trap!

Particularly for folks coming from a REST background, it can be tempting to introduce some kind of enum that allows you to explicitly represent what type of object you're working with.

enum SearchResultType {
  HUMAN
  DROID
  STARSHIP
}

If we go that route, we could add a type: SearchResultType! field to each of our possible search result and then request that field in our query. This has several downsides, though.

First, this isn't a general solution to our problem. If we wanted to be able to tell two kinds of Character apart, we either have to accept that the type field could return STARSHIP (and how would we handle that?), or we have to split our type field out into characterType: CharacterType! and searchResultType: SearchResultType! fields. Neither one of these options scales well as our schema grows, and it leaves us having to manually synchronize enums with our unions and interfaces as our API evolves.

Second, it bypasses the GraphQL runtime's validation of query results. If I write a query like this:

query JediCharacters {
  characters(episode: JEDI) {
    id
    name
    characterType
    ... on Droid {
      primaryFunction
    }
  }
}

There's nothing to promise I won't get a result back that looks like this:

{
  characters: [
    { characterType: "HUMAN", primaryFunction: "etiquette, customs, and translation", /* ... */ }, 
    // ...
  ]
}

It obviously violates the intent of our enum to have a value that must be of type Droid (since it has a primaryFunction) but reports HUMAN as its characterType. This is almost certainly a bug in our implementation, but the GraphQL runtime has no way of knowing that or enforcing that it can't happen.

Finally, for consumers that make use of the GraphQL schema to generate type declarations for working with query results, there's no way for those types to know that characterType: 'DROID' should mean that primaryFunction is a valid property to access.

// generated-schema-types.d.ts
type CharacterType = 'HUMAN' | 'DROID';
type Human = { characterType: CharacterType; height: number; /* ... */ };
type Droid = { characterType: CharacterType; primaryFunction: string; /* ... */ };
type Character = Human | Droid;

// my-code.ts
declare const character: Character;

if (character.characterType === 'DROID') {
  character.primaryFunction; // 💥
  //   Property 'primaryFunction' does not exist on type 'Character'.
  //   ⮑ Property 'primaryFunction' does not exist on type 'Human'.
}

Search Your Feelings, Luke

Sometimes what we're after was there the whole time. It turns out GraphQL adds an implicit field to every object type (including interfaces and unions) called __typename, which is a string representation of the name of the concrete GraphQL type an object has.

This field looks pretty uninviting! In lots of software ecosystems, a leading underscore is an indicator of a private implementation detail, and a double-underscore even more so: "here be dragons" 🐉

However, that's not always the case—sometimes markers like this instead indicate that an identifier is something system-level, like Python's __init__ or JavaScript's __proto__, and this is exactly what's going on with GraphQL. All of its introspection fields and types begin with __, not to indicate that they're private, but to avoid accidentally colliding with regular domain-related types or fields a particular graph might want to expose to its consumers.

All of these __-prefixed identifiers are a defined part of the GraphQL specification, and are explicitly meant for consumers to be able to perform introspection in a safe way. The section specifying the semantics of __typename specifically calls this out:

This is most often used when querying against Interface or Union types to identify which actual type of the possible types has been returned.

This means that rather than trying to capture type information ourselves in the domain-specific layer of the schema, we can (and should) rely on the introspection layer instead.

By doing this, we can avoid the pitfalls described above of using a custom enum. The GraphQL runtime ensures that if a value has __typename: 'Droid', the shape of that object is a valid Droid (or else the entire query will fail with a server error), and we no longer have to do any manual bookkeeping of types since the runtime handles it for us.

The use of __typename in GraphQL tooling is ubiquitous. Client libraries like @apollo/client automatically add __typename to every selection set in a query even if you don't request it in order to power their caching, and code generators like the @graphql-codegen ecosystem intentionally generate types in such a way that __typename acts as a discriminator field, so code like this will "just work":

// generated-schema-types.d.ts
type Human = { __typename: 'Human'; height: number; /* ... */ };
type Droid = { __typename: 'Droid'; primaryFunction: string; /* ... */ };
type Character = Human | Droid;

// my-code.ts
declare const character: Character;

if (character.__typename === 'Droid') {
  character.primaryFunction; // ✅
  // The type system knows `primaryFunction` must exist and be
  // a string within this block, according to the declarations above.
}

Conclusion

If you're looking for a tl;dr, it's this: __typename is your friend, and you shouldn't hesitate to use it in situations where you need to distinguish between different possible types of response you can get back from a GraphQL operation.

It's a public, stable and supported part of the GraphQL spec, and it promises stronger invariants than you can achieve by attempting to roll your own in userspace, so you might as well take advantage of it!

comments powered by Disqus

Recent Posts