DEV Community

Sebastian Döll
Sebastian Döll

Posted on

The Power of TypeScript for GraphQL Union Type Filters

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

Enter fullscreen mode Exit fullscreen mode

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
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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'
            >)
        >
      }
  >
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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

const pages = refs.filter(guardFactory('__typename', 'Page'))
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)