既存プロジェクトに対し、TypeScript設定の厳格化を行う

2021.11.12
既存プロジェクトにはLinterが導入されていませんでした。またTypeScriptのコンパイラの設定が緩い、という課題もありました。この記事では、その二つの課題に対処していきますが、TypeScriptのコンパイラの設定を厳格化する方法をメインに紹介します。

ESLintとは

まずは既存プロジェクトへのESLint導入についてです。

ESLintとはLinterの種類の一つです。Linterとは、ソースコードの問題点などを自動でチェックしてくれるツールです。問題点と言っても、コンパイルは通るけれど、書き方が良くないような箇所を厳しく指摘してくれます。Linterが導入されていると、コードの品質を上げることができます。

ESLintは、javascriptやTypeScriptに対応しているLinterです。ESLintにより、プロジェクト内のコードの書き方を統一することができます。ルールを設定すると、従っていない箇所にエラーを表示してくれます。ESLintのルールには、例えば以下のようなものがあります。

・定数はletではなく、constで定義する。
・使われていない変数は削除する。
・ifやforの条件式で値の代入を行わない
・公開時の消し忘れを防ぐため、consoleの使用を許可しない

また、ESLintには自動でエラーを修正する機能も付いています。terminalで、
eslint --fix
を実行すると自動修正が行われます。

ESLintについてはこちらの記事を参考に導入しました。

既存プロジェクトのTypeScriptコンパイラの設定を厳格化

次にTypeScriptのコンパイラの設定が緩いという課題に対応していきます。プロジェクト開始前にコンパイラ設定を厳格化しておくのが良いのですが、既存プロジェクトのコンパイラ設定をより厳しくしたい場合もあります。これまで緩かったコンパイラの全体設定を途中で厳しくすると、既に書いたコードに対して大量のエラーが発生します。

それらのエラーはコメントを使って一旦は抑制することにしました。しかし、エラーの数は200個近くあり手入力でコメントを挿入するのは大変です。そこでスクリプトを使ってエラー抑制コメントを自動挿入することにしました。エラーを抑制した後は、徐々にコメントを外してエラーを直していきます。

方針としては、以下のような流れで行っていきます。それぞれのやり方について次の段落からもう少し細かく紹介します。

・TypeScriptのコンパイラの設定を変更。
・エラーログのファイル出力
・エラー抑制コメントの自動挿入スクリプトの作成、実行
・コメントを一つずつ外して、エラーを修正

ルールの厳格化とエラー出力

まずコンパイラのオプション設定を変更します。tsconfig.jsonを編集してコンパイラで適用したい設定を行います。厳格なオプションを適用すると多くの箇所でエラーが発生します。
次に、この後のスクリプトで利用するためエラーログをファイル出力します。以下のコマンドでtsc_out.txtにエラーログを出力できます。
npx tsc --noEmit > tsc_out.txt

tsc_out.txtを開くと、以下のような内容のエラーが大量に確認できます。

src/path/file_name.ts(25,17): error TS7006: Parameter 'state' implicitly has an 'any' type.

エラーの抑制

今度は、大量に出ているエラーを抑制していきます。 TypeScriptのコンパイラでは、コメントを使って発生しているエラーを抑制することができます。今回はエラーの発生している箇所に行単位でコメントを挿入してエラーを抑制します。以下の画像にあるコードでは、コンパイラは何型が返ってくるか推定出来ないためエラーとなっています。そこでコメントを挿入することにより、このように一旦エラーを抑制していきます。

スクリプトを使ったエラーの抑制

エラーの発生している箇所は数百個あります。それらの箇所一つ一つに手作業でエラーを抑制するコメントを挿入していくのは大変です。

そこで今回はスクリプトを使って、コメントを自動挿入しました。スクリプトは、次のような処理を行います。

・ファイル出力したエラーログ(tsc_out.txt)を読み込む
・エラーログからエラーの発生しているファイルのパスと行番号を抜き出しオブジェクト形式で記録する
・記録したオブジェクトからパスと行番号を順に読み込む。
・エラーの発生しているファイルにコメントを挿入する。

以下のコードはReactプロジェクトに対して、コメントを挿入してコンパイラのエラーを抑制する際に使用しました。ReactでJSXタグに挟む形でコメントを書く際は、波括弧で囲まないといけないようでした。そのためコメントを追加する一行前の末尾が > で終わっているかで条件分岐している箇所があります。

import fs from 'fs';
import uniq from 'lodash/uniq';

const readData = fs.readFileSync('tsc_out.txt');
const readDataStr = readData.toString();
const outputJson: Record< string, Array> = {};

const readDataStrs = readDataStr.split('src/app/');
readDataStrs.forEach(errorLog => {
  const lineExpPattern = /(\d*,\d*)/;
  const lineResult = errorLog.match(lineExpPattern);
  const pathResult = errorLog.split(' ')[0].split('(')[0];

  if (lineResult != null && pathResult != null) {
    const lineResultNum = parseInt(lineResult[0].split(',')[0]);
    const pathResultFull = 'src/app/' + pathResult;
    if (pathResultFull in outputJson) {
      outputJson[pathResultFull].push(lineResultNum);
      outputJson[pathResultFull] = uniq(outputJson[pathResultFull]);
    } else {
      outputJson[pathResultFull] = [lineResultNum];
    }
  }
});
console.log(outputJson);
Object.entries(outputJson).forEach(([fixFilePath, fixLineArray]) => {
  const readFixFile = fs.readFileSync(fixFilePath);
  const readFixFileStr = readFixFile.toString();
  const fixFileArray = readFixFileStr.split('\n');
  fixLineArray.forEach((lineNum, index) => {
    const checkInJsxTagStr = fixFileArray[lineNum + index - 2];
    if (checkInJsxTagStr.endsWith('>') && !checkInJsxTagStr.endsWith('=>')) {
      fixFileArray.splice(lineNum + index - 1, 0, '{/* @ts-expect-error */}');
    } else {
      fixFileArray.splice(lineNum + index - 1, 0, '/* @ts-expect-error */');
    }
  });
  const fixedStr = fixFileArray.join('\n');
  fs.writeFileSync(fixFilePath, fixedStr);
});

結果は以下のように表示されます。keyにあるのがエラーの発生しているファイルで、valueにある配列が行番号です。どの種類のエラーが発生しているかは扱っていません。

{
'path/to/foo.tsx': [ 9, 19, 25, 67 ],
'path/to/bar.ts': [ 8, 13 ],
'path/to/baz.ts': [ 12 ]
}

スクリプトを実行する際には以下のコマンドを利用します。

npx ts-node main.ts

200箇所くらいあったエラーを2件にまで一旦抑制することができました!