Article summary
Both GraphQL and TypeScript support the concept of discriminated unions (also known as tagged unions). In this post, I’ll walk through setting up an example GraphQL schema and the corresponding TypeScript types, along with writing a query to retrieve a union type.
TypeScript Types
We’ll start with the TypeScript types. The documentation’s Advanced Types page has a “Discriminated Unions” section that uses the following set of types as an example:
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Rectangle | Circle;
GraphQL Schema
GraphQL also supports the concept of unions (see the Apollo documentation or the GraphQL documentation).
Here’s how we could represent our Shape
type in a GraphQL schema:
type Square {
kind: String!
size: Float!
}
type Rectangle {
kind: String!
width: Float!
height: Float!
}
type Circle {
kind: String!
radius: Float!
}
union Shape = Square | Rectangle | Circle
GraphQL Resolver
(I’ve been using Apollo Server for the GraphSQL backend, so this might be a bit Apollo-specific.)
In order to show the resolvers how to resolve the union type, we need to implement a __resolveType
function in our Shape
resolver that knows how to discriminate between the different shapes.
const resolvers = {
Shape: {
__resolveType(obj: { kind: string }, context, info) {
if (obj.kind === 'square') {
return 'Square';
}
if (obj.kind === 'rectangle') {
return 'Rectangle';
}
if (obj.kind === 'circle') {
return 'Circle';
}
return null;
}
}
}
Obviously, you could be more programmatic about converting the value in the kind
field into the name of the GraphQL types, or you could implement this with a switch statement, etc. Since this is an example, I kept this as simple and straightforward as possible.
The Query
The way to query a union type in GraphQL is to specify which fields you want back for each possible type in the union. For example, here’s a GraphQL query that asks for a Shape
, then specifies which fields to include for each kind of possible Shape
. When doing this type of TypeScript integration, you will need to always include the kind
field since that’s what the TypeScript code will use to discriminate between the different types.
query GetBestShape {
shape {
... on Square {
kind
size
}
... on Rectangle {
kind
width
height
}
... on Circle {
kind
radius
}
}
}
For this query, if the resolver for the GetBestShape
query returns a Rectangle
type, then the data sent back to the client will only contain the requested fields in the ... on Rectangle
section. So if you request all of the Rectangle
type’s fields, you can safely assign that to a variable with the TypeScript type of Rectangle
.
That’s all there is to it.
since every square is also a rectangle how would you go about allowing for this? say you wanted to access the length and width properties on a square object could you somehow make that object have both the properties of square and rectangle?
Or in a more real-world example, if you have an interface “person” and you have the types “engineer” and “manager” that inherit from it how would you solve the problem of people who are both engineers and managers?
Was really hoping this would go into checking __typename or each type within the union using resulting types. Typescript doesn’t seem to like that my union types have nullable fields in them, even when I’m checking __typename for success types explicitly. Maybe something to consider for future writing, as pretty much everything out there for using gql unions glosses over it.