The Power of TypeScript for GraphQL Union Type

GraphQL is great. Code generating types for TypeScript is easy. GraphQL supports introspection. This means a GraphQL API provides information about the described schema.

Using the GraphQL CLI and the GraphQL Code Generator generating types for a graphcms is really handy.

# GraphCMS API
schema: https://api-eu-central-1.graphcms.com/v2/xxxxx/master
overwrite: true
documents: ./src/graphql/**/*.graphql

# Format files

extensions:
  codegen:
    hooks:
      afterAllFileWrite:
        - eslint --fix
    generates:
      ./schema.graphql:
        plugins:
          - schema-ast
      src/generated-types.tsx:
        plugins:
          - typescript
          - typescript-operations
          - typescript-react-apollo
        config:
          withHOC: false
          withComponent: true
          withHooks: true

When you add to it the capability to create types for your queries, this is really powerful. A powerful feature of GraphQL are union type. A field can contain different types. For example a Page or Post can can be returned a reference for another page.

query Layout($slug: String = "home") {
  page(where: {slug: $slug}, stage: PUBLISHED) {
    id
    title
    slug
    content
    refs {
      __typename
      ...on Page {
        createdAt
        publishedAt
        pageSlug: slug
        teaser
      }
      ... on Post {
        title
        excerpt
        createdAt
        publishedAt
      }
    }
  }
}

The generated code of the layout query looks like this.

export type LayoutQuery = { __typename?: 'Query' } & {
  page?: Maybe<
    { __typename?: 'Page' } & Pick<
      Page,
      'id' | 'title' | 'slug' | 'content'
    > & {
        refs: Array<
          | ({ __typename: 'Page' } & Pick<
              Page,
              'createdAt' | 'publishedAt' | 'teaser'
            > & { pageSlug: Page['slug'] })
          | ({ __typename: 'Post' } & Pick<
              Post,
              'title' | 'excerpt' | 'createdAt' | 'publishedAt'
            >)
        >
      }
  >
}

Filtering for a Page or Post in the references is not sound, meaning that the TypeScript compiler can tell which type is filtered. A small helper function can provide the necessary information for the compiler so the filtered values are properly typed.

const guardFactory = <T, K extends keyof T, V extends string & T[K]>(
  k: K,
  v: V
): ((o: T) => o is Extract<T, Record<K, V>>) => {
  return (o: T): o is Extract<T, Record<K, V>> => {
    return o[k] === v
  }
}

A filter on the values then returns a properly typed array of Posts or Pages.

const pages = refs.filter(guardFactory('__typename', 'Page'))

This is a really small helper, but it allows to use the full power of TypeScript and GraphQL.

Copyright © 2023. All rights reserved 🤘.