さっそくですが、これはTypeScript 4.4から導入されたuseUnknownInCatchVariablesフラグにより、catch変数の型がunknownになったことが原因のようです。
従来の動作とunknown型への変更
TypeScript 4.3以前では、catch節で補足される変数の型は暗黙的にanyとして扱われていました。これにより、プロパティが存在するかどうかをチェックせずにアクセスできてしまい、意図しないランタイムエラーの原因になることがありました。
従来のコード (TypeScript 4.3以前)
try {
  // 何らかの処理
} catch (err) {
  // err は 'any' 型
  console.error(err.message); // 型チェックなしでアクセス可能
}TypeScript 4.4以降、useUnknownInCatchVariablesが有効だと、この変数のデフォルト型がunknownに変更されたようです。unknown型はany型と異なり、型ガードなどで型を特定しない限りプロパティにアクセスできません。
新しい動作 (TypeScript 4.4以降)
try {
  // 何らかの処理
} catch (error) {
  // error は 'unknown' 型
  console.error(error.message); // コンパイルエラー!
}この変更により、エラーオブジェクトの型を意識した、より安全なコードの記述が求められるようになりました。
型安全なエラーハンドリングの実装方法
unknown型のエラー変数を安全に扱うために、いくつかの実装を書いてみます。
1. instanceof を使った型ガード
最もシンプル?な方法は、errorがErrorクラスのインスタンスであるかを確認することです。
try {
  await someAsyncFunction();
} catch (error) {
  if (error instanceof Error) {
    // 'error' は 'Error' 型として扱える
    console.error(error.message);
  } else {
    console.error('予期しないエラー:', error);
  }
}2. カスタムエラーの型ガード
API通信などで特定の構造を持つエラーオブジェクトが返ってくる場合、それに対応する型ガードを用意すると便利です。
// カスタムエラーのインターフェース
interface ApiError {
  status: number;
  message: string;
}
// 型ガード
function isApiError(error: unknown): error is ApiError {
  return (
    typeof error === 'object' &&
    error !== null &&
    'status' in error &&
    'message' in error
  );
}
// 実装例
try {
  await apiRequest();
} catch (error) {
  if (isApiError(error)) {
    console.error(`API Error [${error.status}]: ${error.message}`);
  } else if (error instanceof Error) {
    console.error(`System Error: ${error.message}`);
  } else {
    console.error('Unknown error:', error);
  }
}tsconfig.jsonでの設定
この動作はstrictオプションがtrueの場合にデフォルトで有効になりますが、個別に制御することも可能です。
{
  "compilerOptions": {
    "strict": true,
    // 個別に設定する場合
    // "useUnknownInCatchVariables": true
  }
}まとめ
catch変数がunknown型になったことで、最初は少し戸惑いましたが、型ガードを適切に行う習慣が身についたと、自分に言い聞かせることにします。
anyで無理やり対応するのも心苦しいので今ではちゃんと書いてます。
