DEV Community

Cover image for How to translate the Gatsby SEO Component to TypeScript
Sascha Metz
Sascha Metz

Posted on • Edited on

How to translate the Gatsby SEO Component to TypeScript

Ever since I consider myself a TypeScript fanboy, when starting a new Gatsby Project with gatsby-starter-default, or similar, I've always had the problem of how to type the SEO Component from Gatsby.

At the beginning of my TypeScript journey, I did the "easy" thing - Setting allowJS: truein my tsconfig and leaving the SEO Component as is... But we all know, this is not a satisfying way of solving problems, so I took the time and want to share my results with you.

TL;DR

import React from "react"
import { Helmet } from "react-helmet"
import { useStaticQuery, graphql } from "gatsby"

export function SEO({
  description = "",
  lang = "en",
  meta = [],
  title,
}: SEOProps) {
  const { site } = useStaticQuery<QueryTypes>(SEOStaticQuery)

  const metaDescription = description || site.siteMetadata.description
  const defaultTitle = site.siteMetadata?.title

  return (
    <Helmet
      htmlAttributes={{
        lang,
      }}
      title={title}
      titleTemplate={defaultTitle ? `%s | ${defaultTitle}` : undefined}
      meta={[
        {
          name: `description`,
          content: metaDescription,
        },
        {
          property: `og:title`,
          content: title,
        },
        {
          property: `og:description`,
          content: metaDescription,
        },
        {
          property: `og:type`,
          content: `website`,
        },
        {
          name: `twitter:card`,
          content: `summary`,
        },
        {
          name: `twitter:creator`,
          content: site.siteMetadata?.author || ``,
        },
        {
          name: `twitter:title`,
          content: title,
        },
        {
          name: `twitter:description`,
          content: metaDescription,
        },
      ].concat(meta)}
    />
  )
}

// Types
type SEOProps = {
  description?: string
  lang?: string
  meta?: Meta
  title: string
}

type Meta = ConcatArray<PropertyMetaObj | NameMetaObj>

type PropertyMetaObj = {
  property: string
  content: string
}

type NameMetaObj = {
  name: string
  content: string
}

type QueryTypes = {
  site: {
    siteMetadata: {
      title: string
      description: string
      author: string
    }
  }
}

// Queries
const SEOStaticQuery = graphql`
  query {
    site {
      siteMetadata {
        title
        description
        author
      }
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

The Props

Let's start with the JS-Version of the component, where the gatsby developers left us a pretty good description of how the props should look like by providing propTypes and defaultProps.

SEO.defaultProps = {
  lang: `en`,
  meta: [],
  description: ``,
}

SEO.propTypes = {
  description: PropTypes.string,
  lang: PropTypes.string,
  meta: PropTypes.arrayOf(PropTypes.object),
  title: PropTypes.string.isRequired,
}
Enter fullscreen mode Exit fullscreen mode

Our TypesScript type would look like this:

type SEOProps = {
  title: string
  description?: string
  lang?: string
  meta?: Meta
}
Enter fullscreen mode Exit fullscreen mode

Since title is the only propType using the TYPE.isRequired property we add a ? to all other keys in our type-object to make them optional.

The Meta Prop

We see the meta-prop is passed through to the Helmet component by using the Array.concat() method to add our values from the meta prop to some defaults. You would assume that the meta prop is just typed as Array, but TypeScript is throwing an error here when passing meta to Helmet. Luckily TypeScript exactly tells you what is expected here:

We can use the ConcatArray Generic here.

We see that the objects inside the meta array are expected in two possible versions:

Version 1

{
  name: "***NAME***"
  content: "***CONTENT***"
}
Enter fullscreen mode Exit fullscreen mode

Version 2

{
  property: "***PROPETY***"
  content: "***CONTENT***"
}
Enter fullscreen mode Exit fullscreen mode

We can describe these objects via types:

type NameMetaObj = {
  name: string
  content: string
}

type PropertyMetaObj = {
  property: string
  content: string
}
Enter fullscreen mode Exit fullscreen mode

Now we can describe our meta property as a ConcatArray Generic with the Union Type of NameMetaObj and PropertyMetaObj as its argument.🧐 In TypeScript, this reads like this:

type Meta = ConcatArray<NameMetaObj | PropertyMetaObj>
Enter fullscreen mode Exit fullscreen mode

Default Props

Using defaultProps in React/TypeScript is as easy as setting them inside the props destructuring in the component definition:

export function SEO({
  description = "",
  lang = "de",
  meta = [],
  title,
}: SEOProps) {
  // Component
}
Enter fullscreen mode Exit fullscreen mode

React Helmet Types

At this point, you may be finished, but in case you haven't installed @types/react-helmet yet, your editor may yell something about not using Helmet as a JSX.Component at you. Just install react-helmet types package and you're good to go.

yarn add @types/react-helmet -D
Enter fullscreen mode Exit fullscreen mode

or

npm install @types/react-helmet --save-dev
Enter fullscreen mode Exit fullscreen mode

Note: If your editor still yells at you, make sure your TypeScript configuration (tsconfig.json e.g.) includes all @types packages.

The Query

We can even get fancier and type the static query because useStaticQuery provides the ability to describe the expected results

type QueryTypes = {
  site: {
    siteMetadata: {
      title: string
      description: string
      author: string
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Provide it to the useStaticQuery call and enjoy the features TypeScript holds for you.

// Inside the Component
const { site } = useStaticQuery<QueryTypes>(SEOStaticQuery)
Enter fullscreen mode Exit fullscreen mode

Wrap Up

That's it for now, feel free to reach out if I missed anything. Please note that this is my first blog post ever... So be kind ;)

Top comments (0)