How to type our GraphQL queries (WP GraphQL + Next.js, part 2)

  • wordpress
  • nextjs
  • graphql
  • typescript
posted on 15th November 2021 ∙ by Hynek Svacha ∙ 12 min read

This is the second part of the ’Create a Next.js static site with Wordpress and GraphQL’ tutorial. In the first part, we learned how to connect WordPress (as a ‘backend’) and Next.js (as a ‘frontend’) using GraphQL queries and how to export our app as a static site. In this part, we will learn how to type our queries with TypeScript.

Prerequisites

The prerequisites are the same as in thefirst part. If you’ve already finished it, you can skip that part. If you don’t want to follow the first tutorial, you can clone the github repo as a starting point:

git clone --recurse-submodules https://github.com/HynekS/wp-next-graphql-tutorial/

You will also need to set up a few more things, though:

WordPress

You will need to create a database for WordPress. In the /bedrock directory, you’ll need to rename the .env.example file to .env. In the .env file, you’ll need to update your database credentials and the base url of the site. For installing all dependencies, you’ll need to run composer install --no-scripts command. You’ll need to activate the ’WP GraphQL’ plugin and flush permalinks (Settings -> Permalinks, Save Changes). It’s probably a good idea to create a few posts using Faker Press plugin (or manually, if you feel like it). For more details, check out the first part.

Next.js

In the /nextjs folder, run yarn install.Start Next.js by running the yarn dev command once all dependencies have been installed and the PHP and MySQL servers for WordPress are up and running.

Install and initialize the GraphQL code generator

The GraphQL Code Generator installation requires a couple of steps. Let’s go through it:

Install the core modules

First, we need to install the core dependencies:

yarn add graphql
yarn add -D @graphql-codegen/cli

Initialize the code generator

Next, we need to initialize the code generator, which will give us a config file:

yarn graphql-codegen init

After that, the init script will ask us several questions about our app. Let’s use the answers as listed below:

Welcome to GraphQL Code Generator! Answer few questions and we will setup everything for you.

? What type of application are you building? Application built with React
? Where is your schema?: (path or url) http://127.0.0.1/wp/graphql
? Where are your operations and fragments?: lib/api.(js|ts)
? Pick plugins:

TypeScript (required by other typescript plugins), TypeScript Operations (operations and fragments)


? Where to write the output: generated/graphql.ts
? Do you want to generate an introspection file? Yes
? How to name the config file? codegen.yml
? What script in package.json should run the codegen? codegen


The code generator will install some additional dependencies and create a new file in our root directory, codegen.yml. It is a config file. If we open it, it should look roughly like this:

# codegen.yml

overwrite: true
schema: "http://127.0.0.1/wp/graphql"
documents: "lib/api.(js|ts)"
generates:
  generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-operations"
  ./graphql.schema.json:
    plugins:
      - "introspection"

Turn GraphQL introspection ON in WP GraphQL plugin settings

There is one important thing we need to update in our WordPress settings. In the bottom of the WP GraphQL plugin settings page, there is a ‘Public Introspection Enabled’ checkbox. For security reasons, it is disabled by default:

One feature of GraphQL is Schema Introspection, which means the GraphQL Schema itself can be queried. This is a feature used by tools such as GraphiQL and others. It’s possible that exposing the Schema publicly (in some cases) can leak information about the system that’s not intended to be known publicly. –WP GraphQL documentation

But we need to enable it—code generators need to know our Schema to generate the types. On our dev environment, there is probably not much to worry about (according to this PR comment, introspection should be disabled by default on staging and production environments). Let’s turn introspection ON and save the changes.

Image of a settings menu with enhanced introspection checkbox

Add the GraphQL Tag Plucks

If we try to run the yarn run codegen script, we’ll still get an error: Unable to find any GraphQL type definitions for the following pointers. That’s because the code generator is trying to find our GraphQL queries to create type definitions, but it can’t find any. All we are pointing it to (the ‘documents’ field in our config) is a file that has a few string arguments that are recognizable as GraphQL queries by a human, but the program is not smart enough (yet). We need to give it a hand.

There are some rather sophisticated solutions to that, like using a custom Webpack loaders for .graphql files, but we’ll keep it simple (and stupid, too, maybe). We’ll give the code generator some hints. These are multiple patterns available; we’ll choose the /* GraphQL */ comment, which we’ll put right before the opening backtick of the query string:

// lib/api.js

export async function getLatestPosts() {
  const data = await fetchAPI(/* GraphQL */ `
    query LatestPosts {
      posts {
        nodes {
          title
          slug
        }
      }
    }
  `);
  return data?.posts;
}

(I’ve noticed that after inserting the /* GraphQL */ hint, the query string was recognized by VS Code as a proper GraphQL query and changed its color. But it might depend on the actual editor settings.)

This method of tagging strings that are actually GraphQL queries is being referred to as a GraphQL Tag Pluck – hence the header caption.

Update the .gitignore file

When running the codegen, we will generate some files that we probably don’t want to be included in our version control. We’ll prevent that by adding these lines to .gitignore:

// ...

# generated schema and types
graphql.schema.json
generated/

Run the codegen script

Great! Everything should be set up now. Let’s try our codegen script.

yarn run codegen

The very first run may take a few seconds. Hopefully, the script resolved successfully, and we can see two more (rather big) files: graphql.schema.json in our root directory, containing our GraphQL schema, and /generated/graphql.ts, containing the derived types. If we open the latter and scroll to the bottom of the file, we should see the types for queries we defined in lib/api.js file:

// generated/graphql.ts

//...

export type LatestPostsQueryVariables = Exact<{ [key: string]: never }>

export type LatestPostsQuery = {
  __typename?: "RootQuery"
  posts?:
    | {
        __typename?: "RootQueryToPostConnection"
        nodes?:
          | Array<
              | {
                  __typename?: "Post"
                  title?: string | null | undefined
                  slug?: string | null | undefined
                }
              | null
              | undefined
            >
          | null
          | undefined
      }
    | null
    | undefined
}

export type AllPostsWithSlugQueryVariables = Exact<{ [key: string]: never }>

export type AllPostsWithSlugQuery = {
  __typename?: "RootQuery"
  posts?:
    | {
        __typename?: "RootQueryToPostConnection"
        nodes?:
          | Array<{ __typename?: "Post"; slug?: string | null | undefined } | null | undefined>
          | null
          | undefined
      }
    | null
    | undefined
}

export type PostQueryVariables = Exact<{
  id: Scalars["ID"]
}>

export type PostQuery = {
  __typename?: "RootQuery"
  post?:
    | {
        __typename?: "Post"
        content?: string | null | undefined
        date?: string | null | undefined
        title?: string | null | undefined
      }
    | null
    | undefined
}

Utilize the types in our api

For simplicity’s sake, we left the /lib/api.js a plain javascript file in the first part of the tutorial. Now, it’s the time to take a step forward and convert it to TypeScript—simply by changing its extension: mv lib/api.js lib/api.ts. The compiler may have some complaints (about parameters without types) —don’t worry, we’ll fix that soon.

Import and arrange types for our queries

First, let’s import our generated types. Also, let’s separate them into two basic categories: the Query types and query variables, which we put inside the general Options type.

// lib/api.ts

import type {
  LatestPostsQueryVariables,
  LatestPostsQuery,
  AllPostsWithSlugQueryVariables,
  AllPostsWithSlugQuery,
  PostQueryVariables,
  PostQuery,
} from "../generated/graphql"

type Query = LatestPostsQuery | AllPostsWithSlugQuery | PostQuery

type Options = {
  variables?: LatestPostsQueryVariables | AllPostsWithSlugQueryVariables | PostQueryVariables
}

const API_URL = "http://127.0.0.1/wp/graphql"

export async function fetchAPI(query, { variables }) {
  // ...
}

// ...

Add types to the FetchApi function

Let’s type out fetchAPI function. It will accept a generic type T with the constraint that it is a T of the Query type: <T extends Query>. The function returns that T type wrapped in a Promise: Promise<T>.

Also, let’s add types to its parameters: the query is simply a string, and the object exposing the variables property will take the Options type. We’ll also give it a default argument—an empty object {}.

// lib/api.ts

import type {
  LatestPostsQueryVariables,
  LatestPostsQuery,
  AllPostsWithSlugQueryVariables,
  AllPostsWithSlugQuery,
  PostQueryVariables,
  PostQuery,
} from "../generated/graphql"

type Query = LatestPostsQuery | AllPostsWithSlugQuery | PostQuery

type Options = {
  variables?: LatestPostsQueryVariables | AllPostsWithSlugQueryVariables | PostQueryVariables
}

const API_URL = "http://127.0.0.1/wp/graphql"

export async function fetchAPI<T extends Query>(
  query: string,
  { variables }: Options = {},
): Promise<T> {
  const headers = { "Content-Type": "application/json" }

  const res = await fetch(API_URL, {
    method: "POST",
    headers,
    body: JSON.stringify({ query, variables }),
  })

  const json = await res.json()

  if (json.errors) {
    console.log(json.errors)
    console.log("error details", query, variables)
    throw new Error("Failed to fetch API")
  }
  return json.data
}

Add types for our queries

Our fetchAPI function accepts a generic type <T>. In the specialized querying functions, we will replace the generic type with one of the concrete types we imported beforehand.

The getPost function also accepts a query variables object as an argument. Let’s give it the imported PostQueryVariables type.

Our api.ts file should look like this (hopefully, no TypeScript complaints):

// lib/api.ts

import type {
  LatestPostsQueryVariables,
  LatestPostsQuery,
  AllPostsWithSlugQueryVariables,
  AllPostsWithSlugQuery,
  PostQueryVariables,
  PostQuery,
} from "../generated/graphql"

type Query = LatestPostsQuery | AllPostsWithSlugQuery | PostQuery

type Options = {
  variables?: LatestPostsQueryVariables | AllPostsWithSlugQueryVariables | PostQueryVariables
}

const API_URL = "http://127.0.0.1/wp/graphql"

export async function fetchAPI<T extends Query>(
  query: string,
  { variables }: Options = {},
): Promise<T> {
  const headers = { "Content-Type": "application/json" }

  const res = await fetch(API_URL, {
    method: "POST",
    headers,
    body: JSON.stringify({ query, variables }),
  })

  const json = await res.json()

  if (json.errors) {
    console.log(json.errors)
    console.log("error details", query, variables)
    throw new Error("Failed to fetch API")
  }
  return json.data
}

export async function getLatestPosts() {
  const data = await fetchAPI<LatestPostsQuery>(/* GraphQL */ `
    query LatestPosts {
      posts {
        nodes {
          title
          slug
        }
      }
    }
  `)
  return data?.posts
}

export async function getAllPostsWithSlug() {
  const data: AllPostsWithSlugQuery = await fetchAPI<AllPostsWithSlugQuery>(/* GraphQL */ `
    query AllPostsWithSlug {
      posts(first: 100) {
        nodes {
          slug
        }
      }
    }
  `)
  return data?.posts
}

export async function getPost(slug: PostQueryVariables["id"]) {
  const data: PostQuery = await fetchAPI<PostQuery>(
    /* GraphQL */ `
      query Post($id: ID!) {
        post(id: $id, idType: SLUG) {
          content
          date
          title
        }
      }
    `,
    {
      variables: {
        id: slug,
      },
    },
  )

  return data?.post
}

Utilize the types in our templates

Great! We already have typed the data we’re querying in our api. But the page templates don’t know the types yet. We need to take one last step.

Import and apply our types

Let’s import InferGetStaticPropsType type utility from the “next” module. Then, we’ll simply use that utility to infer the return type of the getStaticProps function:

// pages/index.tsx

import Link from "next/link"

import type { InferGetStaticPropsType } from "next"

import { getLatestPosts } from "./../lib/api"

const Home = ({ latestPosts }: InferGetStaticPropsType<typeof getStaticProps>) => {
  const { nodes } = latestPosts

  return (
    <div>
      <h1>Our WordPress/Next app Home Page</h1>
      <ul>
        {nodes.map(node => (
          <div key={node.slug}>
            <Link href={"posts/" + node.slug}>{node.title}</Link>
          </div>
        ))}
      </ul>
    </div>
  )
}

export async function getStaticProps() {
  const latestPosts = await getLatestPosts()

  return {
    props: {
      latestPosts,
    },
  }
}

export default Home

Now, we should be able to see the inferred type when hovering over the LatestPosts prop:

Image of the type 'tooltip' appearing on variable hover in VS Code

Update our component to make TypeScript happy

But the TypeScript compiler is not happy! It is spawning wavy red underlines everywhere!

Don’t panic. TypeScript in strict mode (which is the default for Next.js v12) can be pretty harsh. The issue is that very few of the properties in our inferred LatestPosts are guaranteed. Therefore, we need to update our code so it won’t crash at runtime with the well known Uncaught TypeError: Cannot read property 'X' of undefined/null error.

Since our component is simple, all we’ll need are nullish coalescing (??) and optional chaining (?.) operators:

// pages/index.tsx

import Link from "next/link"

import type { InferGetStaticPropsType } from "next"

import { getLatestPosts } from "./../lib/api"

const Home = ({ latestPosts }: InferGetStaticPropsType<typeof getStaticProps>) => {
  const { nodes } = latestPosts ?? {}

  return (
    <div>
      <h1>Our Wordpress/Next app Home Page</h1>
      <ul>
        {nodes?.map(node => (
          <div key={node?.slug}>
            <Link href={"posts/" + node?.slug}>{node?.title}</Link>
          </div>
        ))}
      </ul>
    </div>
  )
}

export async function getStaticProps() {
  const latestPosts = await getLatestPosts()

  return {
    props: {
      latestPosts,
    },
  }
}

export default Home

Now, TypeScript should be happy.

The [slug].tsx template

In the [slug].tsx file, we will need to import one more utility type: GetStaticPropsContext. It will be used for the arguments passed in getStaticProps function. We’re passing in the object literal (for simplicity; it is probably a good practice to extract that into a separate type) with the slug property of type string.

We are also probably getting a lot of TypeScript errors due to unsafe property chaining. Let’s fix it. As before, we’ll use nullish coalescing and optional chaining:

// pages/posts/[slug].tsx

import Link from "next/link"

import type { GetStaticPropsContext, InferGetStaticPropsType } from "next"

import { getAllPostsWithSlug, getPost } from "../../lib/api"

const Post = ({ post }: InferGetStaticPropsType<typeof getStaticProps>) => {
  let { title, date, content } = post ?? {}

  return (
    <div>
      <Link href="/">Back to Homepage</Link>
      <h1>{title}</h1>
      {date && <time dateTime={date}>{new Date(date).toDateString()}</time>}
      <div dangerouslySetInnerHTML={{ __html: content ?? "" }}></div>
    </div>
  )
}

export async function getStaticProps(context: GetStaticPropsContext<{ slug: string }>) {
  const slug = String(context.params?.slug)
  const post = await getPost(slug)
  return {
    props: {
      post,
    },
  }
}

export async function getStaticPaths() {
  const allPostsWithSlug = await getAllPostsWithSlug()
  return {
    paths: allPostsWithSlug?.nodes?.map(node => `/posts/${node?.slug}`) ?? [],
    fallback: false,
  }
}

export default Post

Well, that’s all for now. I hope you’ve learned something useful while reading this. You can check the final code in my github repo.

👍 Enjoy!


If you find anything in this post that should be improved (either factually or in language), feel free to edit it on Github .