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 everyAnimal
will also implementNode
. - Both
AfricanElephant.children
andAlala.children
are invalid, because we have no way to tell GraphQL thatAfticanElephantConnection
andAlalaConnection
are implementations ofAnimalConnection
.
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:
- 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 thecursor
field,AnimalEdge
andAfricanElephantEdge
must also define the cursor field. - All interface implementations declared on the implemented interface must be redeclared on the implementing type or interface. For example, because
Animal
declares that it implementsNode
,AfricanElephant
must also declare that it implementsNode
in addition to implementingAnimal
.
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
.
Top comments (1)
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!