型チェックが緩いTypeScriptプロジェクトでコミット対象ファイルだけ厳格に型チェックしたい

7 min読了の目安(約6500字TECH技術記事 1

はじめに

TypeScriptプロジェクトにおいて、tsconfigのコンパイラオプションでの型に関するオプション(strict, noImplicitAny, strictNullChecks, strictFunctionTypesなど)が有効になっていない場合、型安全性が損なわれてしまいます。せっかくTypeScriptを利用しているので、可能な限り型安全性を高めておきたいです。

そのように型に関するルールが緩いプロジェクトがあった場合、今すぐにでもstrict: trueに変更するのがベストですが、そう簡単に変更できるとは限りません。
ある程度プロジェクトの規模が大きくなると、型安全ではないコードがたくさんあり、修正にも時間と労力がかかってしまいます。

そこで今回は、Gitでコミットするファイルに対して厳密に型チェックして徐々に型安全性を高めていくというアプローチを取るための手法を利用します。[1]

前提

以下のコードがあったとします。

TsconfigErrorExample.ts
const numberList = [5, 12, 8, 130, 44];
const found: number = numberList.find((element) => element > 10);

Array.findメソッドの返り値の型はT | undefinedであり、条件にあう要素がなかった場合にundefinedが返ります。
しかし、コンパイラオプションのstrictfalse(正確にはstrictNullChecksfalse)になっている場合、foundnumber型で定義しているにも関わらず型エラーになりません。(さらに仮に型を定義していなくても、number | undefindではなくnumber型に推論されます。)

strict: trueに設定されていれば、以下のようなエラーになり、処理漏れを防ぐことができます。

Type 'number | undefined' is not assignable to type 'number'.
Type 'undefined' is not assignable to type 'number'.

このように、本当はundefinedになる可能性があるが正しく処理されていないコードに対して、コミット時に検出することを目指します。

準備

では、具体的な方法の解説に移りましょう。

まず、必要なnpmパッケージをインストールします。

npm i -D husky lint-staged
  • husky: git commitする際にlint-stagedを呼ぶためのパッケージ(husky - npm
  • lint-staged: ステージングされたファイルに対して、Linterやフォーマッターの実行を行う。(okonet/lint-staged)
    • 今回はこれを利用して、コミット対象ファイルに対して、型チェックを行います。

検証済みのバージョンは以下です。

  • husky@4.3.6
  • lint-staged@10.5.3
  • typescript@4.1.3

1. huskyの設定

インストールが完了したら、package.jsonに以下を追記します。

package.json
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  }

pre-commit、つまりコミット前にlint-stagedを実行します。

2. tsconfigの設定

lint-stagedで実行するコマンドを定義する前に、型チェックを行うためのtsconfigを設定しましょう。

プロジェクトですでに使っているtsconfig.jsonの他に、lint-staged実行時に使う設定ファイルtsconfig-lint-staged.json(ファイル名は任意)を用意します。

今回は型チェックを厳密に行いたいため、以下のように設定します。

tsconfig-lint-staged.json
{
    "extends": "./tsconfig",
    "compilerOptions": {
        "strict": true
    }
}

extendsオプション(TypeScript: TSConfig Reference - Docs on every TSConfig option)で既存の設定を継承し、変更したい部分だけ指定すると設定が上書きされます。

3. lint-stagedの設定

残るはgit commit前にlint-stagedで実行するコマンドの設定です。

package.jsonを同じ階層にlint-staged.config.jsというファイルを作成します。
拡張子が.tsのファイルに対してtscコマンドを実行します。

lint-staged.config.js
module.exports = {
    '**/*.ts': (fileNames) => [
        'tsc -p tsconfig-lint-staged.json --noEmit',
    ],
};

オプションについては、こちら(TypeScript: Documentation - tsc CLI Options)から確認ください。[2]

(余談)ESLintやPrettierによるフォーマットも同時に行いたい場合

配列に追加すれば順番に実行できます。

lint-staged.config.js
module.exports = {
    '**/*.ts': (fileNames) => [
        'tsc -p tsconfig-lint-staged.json --noEmit',
        `eslint ${fileNames.join(' ')} --fix`,
        `prettier --write ${fileNames.join(' ')}`,
    ],
};

バージョンが古い記事にはgit addが最後に必要と書いてある場合もありますが、lint-stagedのv10では不要になったようです。

From v10.0.0 onwards any new modifications to originally staged files will be automatically added to the commit. If your task previously contained a git add step, please remove this. The automatic behaviour ensures there are less race-conditions, since trying to run multiple git operations at the same time usually results in an error.

ここでのポイントとしては、

  • -pオプションで先ほど作成したtsconfig-lint-staged.jsonを指定すること
  • コマンドはアロー関数で指定すること

です。特に2つ目に関しては、以下のように関数を用いずに文字列として指定するとtscの実行エラーが生じます。

module.exports = {
    '**/*.ts': 'tsc -p tsconfig-lint-staged.json --noEmit',
};
✖ tsc -p tsconfig-lint-staged.json --noEmit:
error TS5042: Option 'project' cannot be mixed with source files on a command line.
husky > pre-commit hook failed (add --no-verify to bypass)

例 (Example: Run tsc on changes to TypeScript files, but do not pass any filename arguments - okonet/lint-staged) にあるようにアロー関数で指定しましょう。

動作確認

設定は以上の3ステップで完了です。それでは動作確認をしましょう。

最初に例にあげた.tsファイルを作成します。

TsconfigErrorExample.ts
const numberList = [5, 12, 8, 130, 44];
const found: number = numberList.find((element) => element > 10);

git addしてステージング後、コミットコマンドを打つと、無事にコンパイルエラーになりコミットを防ぐことができます。

$ git add TsconfigErrorExample.ts

$ git commit -m "lint-staged test"
husky > pre-commit (node v12.16.1)
[STARTED] Preparing...
[SUCCESS] Preparing...
[STARTED] Running tasks...
[STARTED] Running tasks for **/*.ts
[STARTED] tsc -p tsconfig-lint-staged.json --noEmit
[FAILED] tsc -p tsconfig-lint-staged.json --noEmit [FAILED]
[FAILED] tsc -p tsconfig-lint-staged.json --noEmit [FAILED]
[SUCCESS] Running tasks...
[STARTED] Applying modifications...
[SKIPPED] Skipped because of errors from tasks.
[STARTED] Reverting to original state because of errors...
[SUCCESS] Reverting to original state because of errors...
[STARTED] Cleaning up...
[SUCCESS] Cleaning up...

✖ tsc -p tsconfig-lint-staged.json --noEmit:
src/TsconfigErrorExample.ts(4,7): error TS2322: Type 'number | undefined' is not assignable to type 'number'.
  Type 'undefined' is not assignable to type 'number'.
husky > pre-commit hook failed (add --no-verify to bypass)

また、VSCodeのGit機能でコミットしてもエラーになります。(ただし、チェックしている分動作が遅くなります。)

エラーメッセージ

おわりに

型チェックが緩くなってしまっているTypeScriptプロジェクトに対して、これ以上型安全性を低くしないようにするための方法を紹介しました。
今回のアプローチを用いると、少しづつ改善していくことができます。

lint-stagedを用いて、ESLintやPrettierによるチェック・フォーマットも併せて行うことができるので、ぜひ活用してみてください。

読んでいただきありがとうございました。

参考サイト

脚注
  1. LINE証券フロントエンドにおける型安全性への取り組み | LINE DEVELOPER DAY 2020を参考にさせていただきました。こちらではCI環境で型チェックを行い、noImplicitAnyのエラーを減らしていったそうです。 ↩︎

  2. 今回の場合は、tsc --strict --noEmitでも代用可能です。 ↩︎