[TypeScript] 型エラーだらけでもstrict: trueに切り替える

[タイムリープTypeScript 〜TypeScript始めたてのあの頃に知っておきたかったこと〜 Advent Calendar 2021](https://qiita.com/advent-calendar/2021/timeleap-typescript) 13日目です。
本記事は tsconfig で `strict: true` にできていないコードベースを、少しずつ現実的に strict にしていく方法についてご紹介させていただきます。

# 誰のためか
`strict: true` の設定をせずに成長してしまったプロジェクトはありませんか？（私も個人的に経験があります）
例えば以下のようなケースがぱっと思いつきます。

* JavaScript で書いていたプロジェクトに途中から TypeScript を導入したが、型エラーが辛くてルールを弱くしてしまったままにしている。
* TypeScript に慣れてないがゆえに、開発の途中で `strict: true` の設定を外してしまった。

そうして成長してしまったコードベースは、中々ルールを強くするタイミングがなく、TypeScript 型付けの恩恵を100%受けられないまま開発が進んでしまいます。

# なぜ strict: true なのか
`strict: true` にすると、TypeScript の型チェックがより厳密になります。
今どき新しく TypeScript のプロジェクトを始めるときは、基本的に `strict: true` はデフォルトの設定になっていると思います。（例えば React 公式の [Create React App](https://create-react-app.dev/docs/adding-typescript/#installation) など）
型チェックを厳しくすることはプログラムを正しく書く上でとても有用です。
この記事では詳しくは触れませんが、基本的には `strict: true` で開発をすすめるべきです。

# 型エラーをどう潰していくか
すでにコードベースが成長しているプロジェクトのルールを厳しくする際に、すべての型エラーを一気になおすのは現実的ではありません。（事業上、まとまったリファクタ工数は往々にして取れないものですし、全機能の動作検証をするのもとても大変です）
こういったリファクタリングは、日々の開発の中で少しずつ取り組めるのが理想です。その場合に役に立つのが、コンパイラに型エラーを無視してもらう `@ts-` コメントです。
以下のようにすると、行単位 / ファイル単位で型エラーを無視させることができます。

```typescript:行を無視
export function inu(a) { // Error: Parameter 'a' implicitly has an 'any' type.
  console.log(a);
}

// @ts-ignore
export function neko(a) { // No error
  console.log(a);
}
```
```typescript:ファイル全体を無視
// @ts-nocheck
export function neko(a) { // No error
  console.log(a);
}
export function inu(a) { // No error
  console.log(a);
}
```

これを活用することで
　① tsconfig で `strict: true` に切り替える
　② すべてのエラーを`@ts-`コメントで無視する
　③ 日々の開発で触る範囲で、`@ts-`コメントを削除して型を直していく
といった形で少しずつリファクタが進められるようになります。
細かい粒度でリファクタリングを進められたほうが気持ちが楽になるので、ファイル全体ではなく、1行1行に`@ts-`コメントをつけていくほうがおすすめです。

# 一気に`@ts-`コメントをつける
以下のようなスクリプトで一括で`@ts-`コメントを付けることができます😺
**ファイルを書き換えるので、必ずローカルに差分がない状態で実行しましょう！**

```typescript:fixTsErrors.js
/**
 * 実行手順)
 * # 以下、プロジェクトルートで実行
 * $ npx tsc --pretty false > errors.txt　     # 型エラーをファイル出力
 * $ node fixTsErrors.js
 */

const fs = require('fs');

const errorFileContent = fs.readFileSync('./errors.txt', 'utf-8');
const errorLines = errorFileContent.split('\n');

const errors = {};
for (const line of errorLines) {
  const match = line.match(/^(.+)\((\d+),\d+\): error (TS\d+): (.+)$/);
  if (match) {
    const filepath = match[1];
    const row = match[2];
    const code = match[3];
    const message = match[4];

    const e = { code, message };
    if (errors[filepath]) {
      if (errors[filepath][row]) {
        errors[filepath][row].push(e);
      } else {
        errors[filepath][row] = [e];
      }
    } else {
      errors[filepath] = { [row]: [e] };
    }
  }
}

for (const filepath of Object.keys(errors)) {
  const source = fs.readFileSync(filepath, 'utf-8');
  const sourceLines = source.split('\n');
  const fileErrors = errors[filepath];

  let lineOffset = 0;

  const errorRows = Object.keys(fileErrors)
    .map(Number)
    .sort((a, b) => a - b);
  for (const row of errorRows) {
    const errors = fileErrors[row];
    const comments = [
      '// TODO 一時的にルールを無効化しています。気づいたベースで直してください',
    ];
    for (let i = errors.length - 1; i >= 0; i--) {
      const e = errors[i];
      if (i === 0) {
        comments.push(`// @ts-expect-error: ${e.code}: ${e.message}`);
      } else {
        comments.push(`//                   ${e.code}: ${e.message}`);
      }
    }

    sourceLines.splice(row - 1 + lineOffset, 0, ...comments);
    lineOffset += comments.length;
  }

  fs.writeFileSync(filepath, sourceLines.join('\n'));
}
```

ここでは `@ts-ignore` ではなく `@ts-expect-error` を利用しています（TypeScript3.9から利用可能）。
`@ts-ignore` は問答無用で次の行を無視しますが、 `@ts-expect-error` は次の行に型エラーがある場合に限って許容されます。

```typescript
// 次の行に型エラーがある場合は許容される
// @ts-expect-error
export function neko(a) { // No error
  console.log(a);
}

// 次の行に型エラーがない場合にエラーになる
// @ts-expect-error
export function inu(a: string) { // Error: Unused '@ts-expect-error' directive.ts(2578)
  console.log(a);
}
```

ある行の型エラーを直した結果、他の箇所もなおるケースに気付けるようになるのはメリットですね。

# 参考
[公式リファレンス: strict](https://www.typescriptlang.org/tsconfig#strict)
[公式リファレンス: ts-expect-error](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html#ts-ignore-or-ts-expect-error)