Algebraic Data Types in GraphQL
Asked Answered
H

1

6

Is there a technique that would let me declare ADT like this in GraphQL?

// TypeScript
type SomeAlgebraicDataType = 
  | { state: 'A', subState1: string }
  | { state: 'B', subState2: string, subState3: number }
  | { state: 'C', subState4: string, subState5: string, subState6: number }

Note how based on the state discriminator, the rest of the structure can be inferred.

Here's some pseudo code that illustrates the idea:

union AlgebraicDataType = StateA | StateB | StateC

type StateA {state: StateDiscriminator.A, subState1: String }
type StateB {state: StateDiscriminator.B, subState2: String, subState3: Int }
type StateC {state: StateDiscriminator.C, subState4: String, subState5: String, subState6: Int}

enum StateDiscriminator { A B C }
Hetero answered 15/12, 2017 at 22:49 Comment(0)
M
2

According to the spec:

GraphQL Unions represent an object that could be one of a list of GraphQL Object types, but provides for no guaranteed fields between those types. They also differ from interfaces in that Object types declare what interfaces they implement, but are not aware of what unions contain them.

Unions (and interfaces) are referred to in the spec as abstract types because the concrete (i.e. actual) type of a field that returns a Union is not known until runtime. However, Unions also fit the general definition of an algebraic type because they are effectively the sum of two or more object types.

The pseudocode in the question is not far from a working example:

union AlgebraicDataType = StateA | StateB | StateC

type StateA {state: StateDiscriminator, subState1: String }
type StateB {state: StateDiscriminator, subState2: String, subState3: Int }
type StateC {state: StateDiscriminator, subState4: String, subState5: String, subState6: Int}

enum StateDiscriminator { A B C }

The main difference is that when we use SDL to define a schema, we have to specify how the Union is resolved separately from our type definitions. If you're using apollo-server or makeExecutableSchema from graphql-tools, we specify this logic as part of our resolver map:

const resolvers = {
  AlgebraicDataType: {
    __resolveType: (obj) => {
      switch (obj.state) {
        case 'A': return 'StateA'
        case 'B': return 'StateB'
        case 'C': return 'StateC'
        default: {
          throw new TypeError(
            `Unknown state for AlgebraicDataType: ${obj.state}`
          )
        }
      }
    }
  }
}

If you're using vanilla GraphQL.js, the same function is provided to the Union's constructor as the resolveType parameter.

It's also worth noting that if the resolveType function is not provided, by default GraphQL will look for a property named __typename on the provided object and use that to resolve the type. So it's possible to omit the function altogether as long as you return an object with that property inside your resolver.

Mazonson answered 29/7, 2019 at 23:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.