Converting to TypeScript

I used the Gatsby Starter Blog to get this virtual notebook off the ground, which was fabulous for an MVP. Now, though, I want to clean up the code some, starting with converting over to TypeScript.

Install the TypeScript Compiler

npm i --save-dev typescript

Enough said.

Add Type Checking to the Build

Gatsby already supports TypeScript natively, but the docs for gatsby-plugin-typescript point out that it does not do type checking in the build. (It also relies on babel-plugin-transform-typescript rather than the TypeScript compiler, so some features are not supported.)

I want type-checking as part of my build, so I added tsc --noEmit as part of both my develop and build tasks. This doesn’t watch files but performs an initial type check when firing up Gatsby for the first time and, most importantly, before building and deploying.

To get this to work, I also had to run tsc --init to create a basic tsconfig.json file and modify the settings in there to include "allowJS": true. This tells TypeScript to allow both JS and TS files as part of the app (although it doesn’t type-check JS files) unless "checkJS" is also true.) Otherwise, TypeScript was erroring because there was nothing for it to check.

Convert Basic Components to TSX

I started with the smaller styled components and changed the extension from .js to .tsx. I had to further modify my .tsconfig.json to include "jsx": "react" so that TypeScript (and PHPStorm) allowed JSX syntax. For components without props, no other changes were needed.

Convert Layout to TSX

Because Gatsby and TypeScript are configured to allow a mix of JS and TypeScript, I didn’t have to convert everything at once. I could start with the easy components and port the other ones over time.

I did the layout component next, which was the first one to involve props. I added an interface to define the props it took.

interface LayoutProps {
  location: Location;
  title: string;
  sidebar: React.ReactNode;
}

Then I typed the Layout component:

const Layout: React.FC<LayoutProps> 
  = ({ location, title, children, sidebar }) => {
  ...
}

I ran into a few other problems here. I was using Emotion’s css prop, which gave me the type error

Property 'css' does not exist on type 
'DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>'.

In addition, Gatsby’s global __PATH_PREFIX__ was flagged with the error Cannot find name '__PATH_PREFIX__'.

For the css error, I added a @jsx pragma comment to the top of my file, so TypeScript would use Emotion’s JSX

/** @jsx jsx */
import { Global, css, jsx } from "@emotion/react"

I also had to change <> to <Fragment> to avoid the error

An @jsxFrag pragma is required when using an @jsx pragma with 
JSX fragments.

For the second issue, I created a gatsby.d.ts file at the root of my project to handle Gatsby globals. I defined __PATH_PREFIX__ within it:

declare const __PATH_PREFIX__: string

Convert Seo to TypeScript

The one remaining component I had was my SEO component that came with the blog starter. That used React Helmet, so I had to add the type definitions for that module:

npm i --save-dev @types/react-helmet

I added the type definitions for React the same way.

The challenge with this one is that the SEO component was already using PropTypes to type the props for the component.

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

Seo.propTypes = {
  description: PropTypes.string,
  lang: PropTypes.string,
  meta: PropTypes.arrayOf(PropTypes.object),
  title: PropTypes.string.isRequired,
}

The meta prop was passed to the Helmet meta prop, which expected valid HTML meta attributes. I finally landed on the following:

interface MetaObject extends 
  DetailedHTMLProps<
    MetaHTMLAttributes<HTMLMetaElement>, 
    HTMLMetaElement
  >{}

interface SeoProps {
  description?: string;
  lang?: string;
  meta?: MetaObject[];
  title: string;
}

The MetaObject shorthand was a convenience so I didn’t have to keep typing out the full DetailedHTMLProps bit.

Then I used that to type the meta content passed into the prop

let metaContent: MetaObject[] = [...];

However, I kept running into an issue where I couldn’t concatenate any meta passed in as a prop, because the prop type PropTypes.arrayOf(PropTypes.object) wasn’t compatible with my MetaObject. PropTypes.object allowed for an object, null, or undefined to be passed in, but Helmet expected an array of meta attributes, not null or undefined. So while the prop itself is optional, I marked the object as required:

Seo.propTypes = {
  description: PropTypes.string,
  lang: PropTypes.string,
  meta: PropTypes.arrayOf(PropTypes.object.isRequired),
  title: PropTypes.string.isRequired,
}

With that, while I still have the pages to convert, all of the existing components on my journal now make use of Typescript.

References