DEV Community

Mike Marcacci
Mike Marcacci

Posted on

Intermediate Interfaces & Generic Utility Types in GraphQL

Version 15 of graphql-js (which was just released today!) introduces a feature that enables the representation of generic utility types in its type system. It doesn't introduce any new syntax or concepts, but expands the type system's ability to express the relationships between interfaces: an interface can now declare that it implements other interfaces.

To be clear, this is not about creating deep hierarchies; instead, it is a minimal change that enables the expression of a very specific type of relationship that was previously inexpressible.

Interfaces

Let's step back for a minute and talk about interfaces in general. The interface is a particularly useful construct that gives us the ability to define a common shape to be implemented by other types. For example, if we are building an app to help manage a zoo's breeding programs, we might define an Animal interface that contains the basic fields required on animals across all breeding programs:

interface Animal {
  id: ID!
  name: String!
  mother: Animal
  father: Animal
}

Now that we have an Animal interface, we can implement it in types that are specific to each breading program:

type AfricanElephant implements Animal {
  id: ID!
  name: String!
  mother: AfricanElephant
  father: AfricanElephant

  # Fields specific to the african elephant...
  leftTuskLength: Float
  rightTuskLength: Float
}

type Alala implements Animal {
  id: ID!
  name: String!
  mother: Alala
  father: Alala

  # Fields specific to the β€˜alalā...
  beakLength: Float
}

Notice that we needed to redefine each field specified in the interface. Each redefined field must either be exactly the same type as the interface's field, or a refinement of the type defined by the interface. In our example, Animal.father is defined to be of the type Animal, but AfricanElephant.father is defined as AfricanElephant. This is perfectly valid because AfricanElephant implements the Animal interface.

Lists

Now let's extend our schema to introduce a generic utility type: the GraphQL list.

extend interface Animal {
  children: [Animal]
}

extend type AfricanElephant {
  children: [AfricanElephant]
}

extend type Alala {
  children: [Alala]
}

This works perfectly! The GraphQL spec defines a list and makes it clear that the same rules propagate through lists: [AfricanElephant] is a valid refinement of [Animal].

Connections

However, GraphQL has never had a mechanism for extending these rules to custom generic utility types. Instead of using lists directly, let's rework this example to follow the Relay Connection Spec. Let's try writing our server's relay connection implementation in GraphQL:

type PageInfo {
  hasPreviousPage: Boolean!
  hasNextPage: Boolean!
  startCursor: String!
  endCursor: String!
}

interface Node {
  id: ID!
}

interface Edge {
  cursor: String!
  node: Node
}

interface Connection {
  pageInfo: PageInfo!
  edges: [Edge]
}

This is slightly more strict than the spec, and perfectly describes the guarantees our app can make. If you're unfamiliar with connections in GraphQL, Apollo has an excellent writeup that is a perfect primer.

Now let's start creating our edge and connection types:

# Animal
type AnimalEdge implements Edge {
  cursor: String!
  node: Animal
}

type AnimalConnection implements Connection {
  pageInfo: PageInfo!
  edges: [AnimalEdge]
}

extend interface Animal {
  children: AnimalConnection
}

# African Elephant
type AfricanElephantEdge implements Edge {
  cursor: String!
  node: AfricanElephant
}

type AfricanElephantConnection implements Connection {
  pageInfo: PageInfo!
  edges: [AfricanElephantEdge]
}

extend type AfricanElephant implements Node {
  children: AfricanElephantConnection
}

# Alala
type AlalaEdge implements Edge {
  cursor: String!
  node: Alala
}

type AlalaConnection implements Connection {
  pageInfo: PageInfo!
  edges: [AlalaEdge]
}

extend type Alala implements Node {
  children: AlalaConnection
}

We've created all our Edge and Connection types, added our children fields, and extended AfricanElephant and Alala to implement the Node interface... but we've ended up with an invalid schema!

While this was representable using "list" directly, we've lost the ability to convey certain relationships. In particular:

  • AnimalEdge.node is invalid because we have no way to tell GraphQL that every Animal will also implement Node.
  • Both AfricanElephant.children and Alala.children are invalid, because we have no way to tell GraphQL that AfticanElephantConnection and AlalaConnection are implementations of AnimalConnection.

Intermediate Interfaces

And this is what the new changes address. Let's rewrite this using our new ability to tell GraphQL that an interface implements another interface:

# Animal
interface AnimalEdge implements Edge {
  cursor: String!
  node: Animal
}

interface AnimalConnection implements Connection {
  pageInfo: PageInfo!
  edges: [AnimalEdge]
}

extend interface Animal implements Node {
  children: AnimalConnection
}

# African Elephant
type AfricanElephantEdge implements AnimalEdge & Edge {
  cursor: String!
  node: AfricanElephant
}

type AfricanElephantConnection implements AnimalConnection & Connection {
  pageInfo: PageInfo!
  edges: [AfricanElephantEdge]
}

extend type AfricanElephant implements Node {
  children: AfricanElephantConnection
}

# Alala
type AlalaEdge implements AnimalEdge & Edge {
  cursor: String!
  node: Alala
}

type AlalaConnection implements AnimalConnection & Connection {
  pageInfo: PageInfo!
  edges: [AlalaEdge]
}

extend type Alala implements Node {
  children: AlalaConnection
}

It worked: our schema can now use our custom pagination types! We're now using GraphQL to describe the shape of our breeding program data, while also describing the shape of our generic utility types.

By describing these utility types in GraphQL, our tooling can introspect our schema and know exactly what our types do, without relying on descriptions or naming conventions. Our clients also gain the ability to create fragments directly on Edge and Connection interfaces!

The Details

Finally, let's examine the rules for interfaces that implement other interfaces:

  1. All fields of the implemented interface must be redefined with identical or refined types on the implementing type or interface. For example, because Edge defines the cursor field, AnimalEdge and AfricanElephantEdge must also define the cursor field.
  2. All interface implementations declared on the implemented interface must be redeclared on the implementing type or interface. For example, because Animal declares that it implements Node, AfricanElephant must also declare that it implements Node in addition to implementing Animal.

That's it! That's all a schema author needs to know to use this new ability .

Because these rules flatten the schema at the time of authorship, nearly all tooling should continue to work when a schema begins using this pattern. Tooling only needs to change to allow spreads on intermediate interfaces in queries. For implementors of these tools, the introspection schema has a small update: the interfaces field of __Type will now return a list of interfaces when kind is INTERFACE, just like it does when kind is OBJECT.

Links

Top comments (1)

Collapse
 
sfratini profile image
Sebastian Fratini

Thank you for this guide. I was trying to understand the best way to use interfaces and Connections together and this seems like a nice approach.
I was wondering what are your thoughts on a generic Interface for the connection? For example, Having just NodeConnection, NodeEdge, with one Node interface. Of course you'd have to manually use inline fragments on the query but I am trying to avoid duplicating so many interfaces for each pagination query which basically works the same.
Are we losing any features by using a very broad Interface on the node pagination schema?
Thanks!