アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

gatsby-plugin-typescript を試す

本サイトは 2019/1 から GatsbyJS で構築しており、その実装には標準 JavaScript を採用した。しかし最近の私は Node.js も含む JavaScript 系プロジェクトを TypeScript へ移行しており、GatsbyJS も公式プラグイン gatsby-plugin-typescript を利用すれば対応可能なので試してみる。

先に結果を書いておく。開発者にはよいけれど執筆者としては厳しいため移行は取りやめた。

gatsby-plugin-typescript

GatsbyJS サイトの実装を TypeScript 化する場合は gatsby-plugin-typescript を利用する。現時点の README には解説されていないが、この npm から依存するものに peerDependenciestypescript を指定するものがあるため、一緒にインストール。

$ npm i gatsby-plugin-typescript typescript

次に gatsby-config.js へプラグインを登録。他との競合はなさそうなので plugins のどこに定義してもよいが、ビルド環境全体に影響するものなので私は先頭にした。

module.exports = {
  // ...
  plugins: [
    "gatsby-plugin-typescript"
    // ...
  ]
};

公式の解説にはオプションとして

{
  resolve: `gatsby-plugin-typescript`,
  options: {
    isTSX: true, // defaults to false
    jsxPragma: `jsx`, // defaults to "React"
    allExtensions: true, // defaults to false
  }
}

を紹介しているが、私の環境でこれを設定してビルドすると以下の問題に遭遇。これらのオプションを外しても正常にビルド可能なことを確認したので、なにも指定しないようにした。

TypeScript のコンパイル設定。プロジェクト直下に tsconfig.js を配置して以下のように定義。

{
  "compilerOptions": {
    "target": "ES2017",
    "module": "commonjs",
    "jsx": "react",
    "declaration": false,
    "strict": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "typeRoots": ["src/types/"]
  }
}

gatsby-plugin-typescript の README を見るに gatsby-config.js などは対象外のようだ。実際、これを .ts に変えてビルドしようとしたらエラーになった。GatsbyJS v2 時点では React コンポーネントのみを対象と考えておくのがよいだろう。

TypeScript 移行

各種コンポーネント実装を TypeScript へ移行した際の作業メモ。

拡張子の変更

まずはファイルの拡張子を .js から .tsx に変更する。TypeScript は React JSX を利用しているファイルについて .ts ではなく .tsx としなくてはならない。

TypeScript 以外の import 解決

このサイトでは Favicon として favicon.ico 以外にも apple-touch-icon などを指定している。参照する画像は静的コンテンツなので static/ へ配置、参照は ES Modules にして解決を GatsbyJS へ委ねている。

import Favicon from "../static/favicon.ico";
import Favicon32 from "../static/favicon-32.png";

const Header: React.FC<Props> = ({ siteTitle, siteSubTitle }) => (
  <header className="header">
    <Helmet>
      <link rel="icon" type="image/x-icon" href={Favicon} />
      <link rel="icon" type="image/png" sizes="32x32" href={Favicon32} />
    </Helmet>
  </header>
);

しかし TypeScript では .ico.png をサポートしていないので警告される。これを解決するため src/types/index.d.ts を作成し以下のように定義。

declare module "*.ico";
declare module "*.png";

この部分は GatsbyJS 任せで関与しないため、警告を握りつぶしても問題ないと判断した。

props の型を定義

これまでも prop-types を利用して

import React from "react";
import PropTypes from "prop-types";

const Category = ({ categories }) => {};

Category.propTypes = {
  categories: PropTypes.array
};

のように props の型を定義していたのだが、せっかく TypeScript へ移行したので更に厳密化する。本サイトでは gatsby-transformer-remark により Markdown を変換しているので、コンポーネントが参照するデータは GraphQL だと

export const query = graphql`
  query IndexQuery {
    site {
      siteMetadata {
        blogTitle
        subtitle
        description
        copyright
        repositoryName
        repositoryLink
      }
    }
    allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
      totalCount
      edges {
        node {
          id
          frontmatter {
            title
            date(formatString: "MMMM DD, YYYY")
            path
            categories
            tags
            excerpt
            single
          }
        }
      }
    }
  }
`;

のようになる。更に gatsby-node.js でカテゴリーやタグのページ用に独自データを受け渡せるようにしているので、これらを含め型を定義。今回は前述の index.d.tsdeclare 専用とし、サイトのデータは同階層の data.d.ts というファイルにしておく。参照側で独立した型としたい単位で型付けしたところ、以下のようになった。

export type Frontmatter = {
  title: string;
  date: string;
  path: string;
  categories: string[];
  tags: string[];
  excerpt: string;
  single: boolean;
};

export type EdgeNode = {
  id: number;
  frontmatter: Frontmatter;
};

export type SiteData = {
  site: {
    siteMetadata: {
      title: string;
      blogTitle: string;
      subtitle: string;
      description: string;
      copyright: string;
      repositoryName: string;
      repositoryLink: string;
    };
  };
  allMarkdownRemark: {
    totalCount: number;
    edges: {
      node: EdgeNode;
    }[];
  };
  markdownRemark: {
    frontmatter: Frontmatter;
    html: string;
  };
};

export type PageContext = {
  categories: string[];
  tags: string[];
  prev: Frontmatter | null;
  next: Frontmatter | null;
  posts: EdgeNode[] | null;
  categoryName: string;
  tagName: string;
};

コンポーネント側の参照はこのような感じ。

import React from "react";
import { SiteData } from "../types/data";

type Props = {
  data: SiteData;
};

const IndexPage: React.FC<Props> = ({ data }) => (
  <div className="page">
    <div className="container">
      <Helmet title={data.site.siteMetadata.title} />
    </div>
  </div>
);

tsc による型チェックはもちろん、VS Code 上なら入力補完も効いて快適。今回は既に動作していた実装を移植したのでありがたみは薄いが、新規だとか別コンポーネントを追加した際には型が大いに役立つだろう。

それとこの作業をしていて、GatbyJS の特徴である GraphQL を用いたデータ形式の明示はよいものだと再確認させられた。型定義では手抜きして全コンポーネントの GraphQL 集合を満たすようにしたが、そうだとしても各種クエリーが実際に利用している部分は宣言されているし、厳密にゆくなら GraphQL 単位で型定義して組み合わせる手もある。どちらにせよ明示的・自己言及的でコード読者フレンドリーだ。

それだけに gatsby-node.js などを TypeScript 化できないのは実に惜しい。コンポーネントへ渡す元データを生成するこの箇所こそ、型チェックと入力補完が活きる部分だというのに。

問題点

開発の観点からは移行してよかったけれど gatsby develop 時の更新反映が非常に遅い。例えば本記事を変更して保存すると

info added file at .../src/pages/blog/2019/10/21.md
success write out requires - 0.009s
success run queries - 7.477s - 2/2 0.27/s
success run queries - 0.197s - 4/4 20.30/s
success write out requires - 0.007s

こんなに時間がかかっている。7 秒超えはリアルタイム プレビューとして致命的だ。ゆるくない。そのため結局、元に戻してしまった。同一環境で TypeScript なしだとこんな感じ。

info added file at .../src/pages/blog/2019/10/21.md
success write out requires - 0.013s
success run queries - 1.554s - 2/2 1.29/s
success run queries - 0.238s - 5/5 21.04/s
success write out requires - 0.011s

1 秒は超えていて視認可能な速度ではあるけど、これぐらいなら数文節を書いて保存するぐらいの猶予はあるから許容範囲だ。

まとめ

TypeScript 化できて既存の記事が表示されるまでは喜んだ。今後、なにかコンポーネントを追加変更するなら便利だと。しかし作業内容を記事にしてプレビューしたら、パフォーマンス面で残念な結果になった。課題をまとめる。

  • gatsby-node.js などを TypeScript 化できず、対応として半端
  • 記事ビルドが遅すぎてリアルタイム プレビューは厳しい

これらが解決したら、改めて移行を検討するかもしれない。