Questo articolo rappresenta la traduzione in italiano del post originale Get a catch block error message with TypeScript di Kent C. Dodds

Parliamo di questo esempio:

const reportError = ({ message }) => {
  // invia l'errore al nostro servizio di logging...
}

try {
  throw new Error('Oh no!')
} catch (error) {
  // procediamo, ma segnaliamo l'errore
  reportError({ message: error.message })
}

Fin qui tutto bene? Beh, questo è perché stiamo usando JavaScript. Vediamo cosa succede con TypeScript:

const reportError = ({ message }: { message: string }) => {
  // invia l'errore al nostro servizio di logging...
}

try {
  throw new Error('Oh no!')
} catch (error) {
  // procediamo, ma segnaliamo l'errore
  reportError({ message: error.message }) // Errore di TypeScript!
}

La chiamata a reportError non compila. Il colpevole è error.message. Questo perché (da poco) TypeScript imposta di default il tipo di error su unknown. Ed è davvero così! Nel mondo degli errori, non ci sono molte garanzie sui tipi di errori che possono essere lanciati. Infatti, questo è lo stesso motivo per cui non puoi fornire il tipo per il .catch(error => {}) di una Promise rejection con il generico della Promise (Promise<ResolvedValue, NopeYouCantProvideARejectedValueType>).

In effetti, potrebbe non essere nemmeno un errore quello che viene lanciato. Potrebbe essere praticamente qualsiasi cosa:

throw 'Che succede!?'
throw 7
throw { cosa: 'è questo' }
throw null
throw new Promise(() => {})
throw undefined

Puoi lanciare qualsiasi cosa di qualsiasi tipo. Quindi è facile, giusto? Potremmo semplicemente aggiungere il tipo Error al catch per dire che questo codice lancerà solo un errore, giusto?

try {
  throw new Error('Oh no!')
} catch (error: Error) { // <-- Aggiungto qui 
  // segnaliamo l'errore
  reportError({ message: error.message })
}

Non così in fretta! Con questo otterrai il seguente errore di compilazione TypeScript:

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

Il motivo è che anche se nel nostro codice sembra che non ci sia modo che venga lanciato qualcos’altro, JavaScript è un po’ strano e quindi è perfettamente possibile per una libreria di terze parti fare qualcosa di strano come fare il monkey-patching del costruttore di Error per lanciare qualcosa di diverso:

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

Quindi cosa deve fare uno sviluppatore? Il meglio che può! Che ne dici di questo:

try {
  throw new Error('Oh no!')
} catch (error) {
  let message = 'Errore Sconosciuto'
  if (error instanceof Error) message = error.message
  // procediamo, ma segnaliamo l'errore
  reportError({ message })
}

Ecco fatto! Ora TypeScript non si lamenta più e, cosa più importante, stiamo gestendo i casi in cui potrebbe essere qualcosa di completamente inaspettato. Forse potremmo fare anche di meglio:

try {
  throw new Error('Oh no!')
} catch (error) {
  let message
  if (error instanceof Error) message = error.message
  else message = String(error)
  // procediamo, ma segnaliamo l'errore
  reportError({ message })
}

Quindi qui se l’errore non è un vero oggetto Error, allora lo convertiamo semplicemente in stringa e speriamo che finisca per essere qualcosa di utile.

Possiamo trasformare questo in una utility da utilizzare in tutti i nostri blocchi catch:

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

const reportError = ({ message }: { message: string }) => {
  // invia l'errore al nostro servizio di logging...
}

try {
  throw new Error('Oh no!')
} catch (error) {
  // procediamo, ma segnaliamo l'errore
  reportError({ message: getErrorMessage(error) })
}

Questo mi è stato utile nei miei progetti. Spero che aiuti anche te.

Aggiornamento

Nicolas ha avuto un ottimo suggerimento per gestire situazioni in cui l’oggetto errore con cui stai lavorando non è un vero errore. E poi Jesse ha suggerito di stringificare l’oggetto errore se possibile. Quindi tutti insieme i suggerimenti combinati appaiono così:

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 nel caso ci sia un errore nel cast a string di maybeError
    // come dei riferimenti circolari per esempio.
    return new Error(String(maybeError))
  }
}

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

Utile!

Conclusione

Penso che la lezione chiave qui sia ricordare che mentre TypeScript ha i suoi aspetti particolari, non dovresti ignorare un errore o un avviso di compilazione da TypeScript solo perché pensi che sia impossibile che si verifichi. La maggior parte delle volte è assolutamente possibile che accada l’inaspettato e TypeScript fa un ottimo lavoro nel costringerti a gestire quei casi improbabili… E probabilmente scoprirai che non sono così improbabili come pensi.