TypeScriptにおけるcatchブロックでのエラーメッセージ取得

この記事はKent C. DoddsによるGet a catch block error message with TypeScriptの翻訳です。

さて、ここから始めましょうか。

const reportError = ({message}) => {
  // send the error to our logging service...
}

try {
  throw new Error('Oh no!')
} catch (error) {
  // we'll proceed, but let's report it
  reportError({message: error.message})
}

今のところ順調?まあ、それはJavaScriptだからです。ここにTypeScriptを投下しましょう。

const reportError = ({message}: {message: string}) => {
  // send the error to our logging service...
}

try {
  throw new Error('Oh no!')
} catch (error) {
  // we'll proceed, but let's report it
  reportError({message: error.message})
}

`reportError`の呼び出しは嬉しくありません。特に`error.message`のところです。それは(最近の)TypeScriptが`error`の型を`unknown`にしているからです。というのは、本当にそうなんです!エラーの世界において、投げられるエラーの型をあなたが提供できる保証などありません。実は、同じ理由でプロミスジェネリック(`Promise<ResolvedValue, NopeYouCantProvideARejectedValueType>`).を使ってプロミス拒否である`.catch(error => {}`に型を提供できません。実際、投げらるのがエラーでないことさえあります。なんでもいいのです。

throw 'What the!?'
throw 7
throw {wut: 'is this'}
throw null
throw new Promise(() => {})
throw undefined

真面目な話、どんな型のなんであっても投げることができます。だから簡単でしょう?このコードはエラーしか投げないというエラー用の型アノテーションを追加すればいいんですよね?

try {
  throw new Error('Oh no!')
} catch (error: Error) {
  // we'll proceed, but let's report it
  reportError({message: error.message})
}

そうはいかない!それをするとあなたは以下のTypeScriptコンパイルエラーを受け取ります。

Catch clause variable type annotation must be 'any' or 'unknown' if specified. ts(1196)

この理由は、コードでは他に投げられるものがないように見えても、JavaScriptはちょっとおかしいので、サードパーティーのライブラリがエラーコンストラクターにモンキーパッチを当てて別のものを投げさせるようなおかしなことをする可能性が確実にあるからです。

Error = function () {
  throw 'Flowers'
} as any

さて、デベロッパーはどうすべきでしょうか?何か最高なものを!さて、こんなのはどうでしょう?:

try {
  throw new Error('Oh no!')
} catch (error) {
  let message = 'Unknown Error'
  if (error instanceof Error) message = error.message
  // we'll proceed, but let's report it
  reportError({message})
}

よし!TypeScriptはもう文句を言ってこないし、もっと重要なのは私たちは全く予想外の何かに対しても対応しているということです。でも、もっといいものができるかもしれません:

try {
  throw new Error('Oh no!')
} catch (error) {
  let message
  if (error instanceof Error) message = error.message
  else message = String(error)
  // we'll proceed, but let's report it
  reportError({message})
}

そこで、ここでは、エラーが実際の`Error`オブジェクトでない場合、そのエラーを文字列化しうまくいけばそれが役に立つものになるようにします。
そして、これをユーティリティにすることですべてのキャッチブロックで使用することができます。

function getErrorMessage(error: unknown) {
  if (error instanceof Error) return error.message
  return String(error)
}

const reportError = ({message}: {message: string}) => {
  // send the error to our logging service...
}

try {
  throw new Error('Oh no!')
} catch (error) {
  // we'll proceed, but let's report it
  reportError({message: getErrorMessage(error)})
}

これは私のプロジェクトの中でとても便利なものになっています。あなたのプロジェクトの役にも立てれば幸いです。

更新:Nicolasがエラーオブジェクトが本当のエラーではない場合の対応について素晴らしい提案をしてくれましたそして、Jesseが文字列化が可能な際のやり方について提案してくれました。すべての提案を組み合わせるとこのようになります:

type ErrorWithMessage = {
  message: string
}

function isErrorWithMessage(error: unknown): error is ErrorWithMessage {
  return (
    typeof error === 'object' &&
    error !== null &&
    'message' in error &&
    typeof (error as Record<string, unknown>).message === 'string'
  )
}

function toErrorWithMessage(maybeError: unknown): ErrorWithMessage {
  if (isErrorWithMessage(maybeError)) return maybeError

  try {
    return new Error(JSON.stringify(maybeError))
  } catch {
    // fallback in case there's an error stringifying the maybeError
    // like with circular references for example.
    return new Error(String(maybeError))
  }
}

function getErrorMessage(error: unknown) {
  return toErrorWithMessage(error).message
}

便利!

結論

今回の重要な収穫は、TypeScriptには少々おかしなところがある一方でTypeScriptのコンパイルエラーや警告を、ありえないなどといういう理由で否定しないことだと思います。ほとんどの場合、予期せぬことが起こる可能性が確実にあり、TypeScriptはそのようなありえないケースに対処するよう、かなり良い仕事をしてくれるのです...。そしておそらく、そのような場面はあなたが思っているほどありえないことでもないことに気付くでしょう。

いいなと思ったら応援しよう!