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.