JAMStack作成の軌跡Part2

## はじめに

- [前回](https://qiita.com/krkettle57/items/db23c433ec4ea7fb26e4)でGatsbyの環境構築を行った
- 今回はTypeScriptの恩恵を受けられるようにESLint, Prettierの設定を行う
  - [GatsbyをTypeScript化してESLintとPrettierを導入する](https://kohsuk.tech/2020/12/5/)が非常に参考になった
- 普段はVSCodeを利用しているため、VSCodeの拡張機能も設定する
- 完成形は[こちら](https://github.com/krkettle57/gatsby-sample/tree/blog-part2)

## TypeScriptの導入

GatsbyにはデフォルトでTypeScriptに対応しているが、型チェックの恩恵を十分に得られないため、DX(Developer Experience)を向上させるために追加の設定を行っていく。

### tsconfig.jsonの作成

```json
{
  "include": ["./src/**/*"], // コンパイル対象
  "compilerOptions": {
    "target": "esnext", // 出力するjsのバージョン
    "module": "esnext", // 出力するjsのモジュール
    "lib": ["dom", "es2017"], // コンパイルする際に使用する組み込みライブラリ
    "jsx": "react", // tsxファイルの出力形式
    "esModuleInterop": true, // CommonJSモジュールのインポートをサポートする
    "forceConsistentCasingInFileNames": true, // 大文字小文字を区別して参照解決する
    "strict": true, // 型チェックをstrictモードにする
    "skipLibCheck": true, // 型定義ファイルの整合性チェックを行わない
    "noEmit": true, // コンパイル結果を出力しない
    "moduleResolution": "node"// tscのモジュール解決方法(新しい環境で行う場合はnodeを指定する)
  }
}
```

- 参考
  - [tsconfig.jsonの全オプションを理解する（随時追加中）](https://qiita.com/ryokkkke/items/390647a7c26933940470)
  - [esModuleInterop オプションの必要性について](https://qiita.com/karak/items/29ff148788f5abb15331)

### .js -> .tsxへ変更する

- src/下の.jsファイルを.tsxに変更する
- この状態で`yarn develop`をしても問題なく動作するが静的解析の恩恵は得たい

## ESLintの設定

- JS のための静的検証ツール
- Pythonのflake8みたいなやつ

```shell
yarn add -D eslint eslint-loader
yarn run eslint --init

# 以下、選択内容
? How would you like to use ESLint? … 
  To check syntax only
  To check syntax and find problems
❯ To check syntax, find problems, and enforce code style

? What type of modules does your project use? … 
❯ JavaScript modules (import/export)
  CommonJS (require/exports)
  None of these

? Which framework does your project use? … 
❯ React
  Vue.js
  None of these
  
? Does your project use TypeScript? › No / [Yes]

? Where does your code run? …  (Press <space> to select, <a> to toggle all, <i> to invert selection)
✔ [Browser]
✔ Node

? How would you like to define a style for your project? … 
❯ Use a popular style guide
  Answer questions about your style
  Inspect your JavaScript file(s)
  
? Which style guide do you want to follow? … 
❯ Airbnb: https://github.com/airbnb/javascript
  Standard: https://github.com/standard/standard
  Google: https://github.com/google/eslint-config-google
  XO: https://github.com/xojs/eslint-config-xo
  
? What format do you want your config file to be in? … 
  JavaScript
  YAML
❯ JSON

? Would you like to install them now with npm? › No / [Yes]
```

- この状態で`yarn run eslint --fix "**/*.tsx"`を実行すると大量のエラー文が出力される
  - .cache/, public/もESLintの対象になっているが後述のtsconfig.eslint.jsonで対象外にする

### ESLintで出るエラーの解消するために.eslintrc.jsonを修正する

```json
{
    "extends": [
        // 省略
        "plugin:import/errors", // 追加
        "plugin:import/warnings", // 追加
        "plugin:import/typescript", // 追加
    ],
    "rules": {
        "no-use-before-define": "off", // 追加
        "@typescript-eslint/no-use-before-define": ["error"], // 追加
        "react/jsx-filename-extension": ["error", { "extensions": [".jsx", ".tsx"] }], // 追加
        "react/no-unescaped-entities": "off", // 追加
        "react/prop-types": "off" , // 追加
        "react/require-default-props": "off", // 追加
        "import/extensions": [ // 追加
            "error",
            "ignorePackages",
            {
              "js": "never",
              "jsx": "never",
              "ts": "never",
              "tsx": "never"
            }
        ]
    },
}

```

- この状態で`yarn run eslint --fix "**/*.tsx"`を実行するとエラーは全て消えているが、型が指定されていなかったり、そもそもインストールされていなかったりする(react-helmetなど)

- これをチェックするため、eslintrc.jsonにさらに以下を追加すると型が指定されていないエラーなどが出現する

```json
"extends": [
// 省略
"plugin:@typescript-eslint/recommended", // 追加
"plugin:@typescript-eslint/recommended-requiring-type-checking", // 追加
],
"parserOptions": {
// 省略
"project": "./tsconfig.eslint.json" // 追加
},
```

- parserOptions.projectに設定したtsconfig.eslint.jsonでESLint対象のファイルを設定する
```json
{
  "extends": "./tsconfig.json",
  "include": ["./src/**/*"],
}
```



## TypeScirptへの本格対応

- TypeScriptの型チェックが正しく効くようになったため、ソースコードを修正していく

### src/componentsの修正

- src/components/seo.tsxを例にとる(他のcomponentsも同様に修正)
- 修正ポイントは以下の通り
  1. react-helmetの型定義
  2. Seo関数の型定義
  3. GraphQLの型定義

#### react-helmetの型定義

```tsx
import { Helmet } from "react-helmet";
```

- react-helmetの定義ファイルがないために、VSCodeがエラーを吐いている
- これは定義ファイルをインストールすれば解決

```
yarn add -D @types/react-helmet
```

#### Seo関数の型定義

- Seo関数の型定義がされていないため、propsとSeoの型を定義する
- Seo.defaultProps, Seo.propTypesは不要のため削除
- 参考にした記事
  - [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app/docs/advanced/patterns_by_usecase)
  - [結局 React のコンポーネントにはどうやって型をつければ良いのさ](https://kk-web.link/blog/20201023)
  - [一番文句言われなさそうな React コンポーネントの書き方](https://zenn.dev/seya/articles/0317b7a61ee781)
  - [Reactのコンポーネント周りの用語を整理する](https://blog.ojisan.io/react-component-words/)
  - [【TypeScript】React.VFCとは何ぞや](https://qiita.com/tttocklll/items/c78aa33856ded870e843)

```tsx
type SEOProps = {
  title: string;
  lang?: string;
  meta?: React.ComponentPropsWithoutRef<'meta'>[];
  description?: string;
};

const SEO: React.VFC<SEOProps> = ({
  title,
  lang = 'ja',
  meta = [],
  description = '',
}) => {
  // 省略
}
```

#### GraphQLの型定義

- GraphQLで得られたレスポンスの型を[gatsby-plugin-typegen](https://github.com/cometkim/gatsby-plugin-typegen)で生成する

```shell
yarn add gatsby-plugin-typegen
```

- gatsby-config.jsにpluginを追加

```js
plugins: [
    // 省略
    `gatsby-plugin-typegen`,
]
```

- `yarn build`を実行し、`src/__generated__`ディレクトリが作成されることを確認する
- これでエラーは消える(消えない場合は`Ctrl + Shift + P`などでVSCodeのWindowを再起動してみる)

### src/pagesの修正

- src/pages/index.tsxを例にとる(他のpagesも同様に修正)
- IndexPage関数の型定義を加えればOK

```tsx
const IndexPage: React.VFC<PageProps> = () => (
  // 省略
)
```



## Prettierの設定

- JSのためのコードフォーマッタ
- Pythonのblackみたいなやつ
- prettierはデフォルトでインストールされている
- 設定ファイル.prettierrcを修正する
- [Prettier](https://prettier.io/docs/en/configuration.html)

```.prettierrc
{
  "arrowParens": "avoid",
  "semi": true,
  "singleQuote": false,
  "tabWidth": 2,
  "trailingComma": "es5"
}
```

### ESLintとPrettierの競合回避

```shell
yarn add -D eslint-config-prettier
```

- eslintrc.jsonのextendsにprettierを追加

```json
"extends": [
  // 省略
  "prettier"
],
```

- この状態で`yarn run prettier --write "**/*.tsx"`を実行するとコードフォーマットが実行される

## パッケージ追加 & package.jsonの設定

- ESLint, Prettierを実行したい時に長ったらしいコマンドを打つのは面倒
- コミットする前にELint, Prettierが確実に実行されるようにしてコードの品質を保ちたい

### ESLint, Prettierの実行を楽にする

- package.jsonにスクリプトを登録しておく

```json
"scripts": {
    // 省略
    "format:prettier": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
    "format:eslint": "eslint --fix",
    "format": "yarn format:eslint && yarn format:prettier"
}
```

- これで`yarn format`を実行すればESLintもPrettierも実行される

### (Optional) コミット前にESLint, Prettierが実行されるようにする

- ※現状ESLintでエラーが吐かれている状態のため、以下の設定をすると**解消するまでコミットができなくなる**ので注意
- 必要なパッケージをインストールする
  - [husky v6 のインストール方法と使い方。lint-staged も導入して、品質を保とう](https://fwywd.com/tech/husky-setup)が参考になった！
  - yarnのバージョンが異なると手順が異なるようなので、詳細は↑を参照

```shell
# yarn v1の場合
yarn add -D husky lint-staged
yarn husky install
yarn husky add .husky/pre-commit "yarn lint-staged"
```

- Package.jsonに設定を記述しておく

```json
"scripts: {
    // 省略
},
"lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write \"**/*.{js,jsx,ts,tsx}\"",
    ],
    "*.{json,md}": [
      "prettier --write \"**/*.{json,md}\""
    ]
},
```

## VSCodeの設定

- VSCodeでリアルタイムコード解析がされるようにする
- VSCodeで保存時に自動でESLint, Prettierを自動実行してくれるようにする

### 拡張機能のインストール

以下の拡張機能をインストールするとリアルタイムコード解析がされるようになる

- ESLint: dbaeumer.vscode-eslint
- Prettier - Code formatter: esbenp.prettier-vscode

### 保存時にESLint, Prettierを自動実行するように設定する

- ※以下で行う設定を有効化するには**開いているWorkspase直下に後述する.vscodeが存在している必要がある**ので注意

- プロジェクトルート直下に.vscodeフォルダを作成し、.vscode直下に以下のsettings.jsonを作成する

```json
{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll": true,
    "source.organizeImports": true
  },
}
```

-  蛇足だが.vscode直下にextensions.jsonを作成しておくと推奨拡張機能などを記述できるため、チーム開発等に便利
   - 詳しくは[公式ドキュメント](https://code.visualstudio.com/docs/editor/extension-marketplace#_workspace-recommended-extensions)を参照
-  VSCodeのコード解析をすると、赤線が引かれているところが散見されるため次項で対応する

### tscの対応

- TypeScriptによるコンパイルチェックとESLintの静的解析は内容が異なる
  - 具体例: `Object is possibly 'undefined'.ts(2532)`
  - 参考: [ESLint not reporting TypeScript compiler type checking errors](https://stackoverflow.com/questions/60514929/eslint-not-reporting-typescript-compiler-type-checking-errors)
- まずはこれを`yarn format`で検出できるようにpackage.jsonを修正しておく
  - `lint-staged`と組み合わせると`tsc`がうまく動作せず`error TS5042: Option 'project' cannot be mixed with source files on a command line.`などのエラーが発生してしまう
  - そこでbashをかませることでエラーを回避する
  - 参考: [lint-staged ignores tsconfig.json when it called through husky hooks](https://github.com/okonet/lint-staged/issues/825#issuecomment-674575655)

```json
"scripts": {
    // 省略
    "check-types": "tsc --noemit", // 追加
    "format:prettier": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
    "format:eslint": "eslint --fix",
    "format": "yarn format:eslint && yarn format:prettier && yarn check-types", // 修正
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write \"**/*.{js,jsx,ts,tsx}\"",
      "bash -c tsc --noEmit" // 追加
    ],
  },
```

- 次にエラーが出ないように修正していく
  - 参考: [【Typescript】Nullableに対する様々な操作](https://blog.narumium.net/2020/01/23/%E3%80%90typescript%E3%80%91nullable%E3%81%AB%E5%AF%BE%E3%81%99%E3%82%8B%E6%A7%98%E3%80%85%E3%81%AA%E6%93%8D%E4%BD%9C/)
  - またsrc/components/seo.tsxを例にとる(他のcomponentsも同様に修正)

- GraphQLのレスポンスとして`undefined`かもしれないオブジェクトは?をつけてNull許容型にする

```tsx
const metaDescription = description || site?.siteMetadata?.description;
```

- titleTemplateの型を合わせるためにnullからundefinedに変更する

```tsx
<Helmet
  htmlAttributes={{
    lang,
  }}
  title={title}
  titleTemplate={defaultTitle ? `%s | ${defaultTitle}` : undefined} // null -> undefined
  meta={defaultMeta.concat(meta)} // defaultMetaは後述
/>
```

- metaの型を明示する

```tsx
const defaultMeta: React.ComponentPropsWithoutRef<"meta">[] = [
  {
    name: "description",
    content: metaDescription,
  },
  // 省略
]
```