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 Post
s or Page
s.
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.