almessadi.
Retour à l'index

Types de Résultat pour les Échecs Attendus en TypeScript_

Les exceptions ont toujours leur place, mais les échecs commerciaux attendus sont plus faciles à raisonner lorsqu'ils sont modélisés comme des données plutôt que cachés dans le flux de contrôle.

Publié12 avril 2024
Temps de lecture10 min read

TypeScript vous offre de fortes garanties concernant de nombreux types d'entrées invalides.

Ce qu'il ne fait pas, c'est suivre les exceptions levées dans votre programme de manière utile.

C'est pourquoi les échecs attendus se lisent souvent plus clairement comme des données que comme des exceptions.

Tous les Échecs Ne Devraient Pas Lever d'Exceptions

Il y a une différence entre :

  • "la passerelle de paiement a renvoyé carte_refusée"
  • "notre processus a rencontré une invariant qui ne devrait jamais se produire"

Le premier fait partie du flux commercial normal.
Le second est exceptionnel.

Si vous lancez une exception pour les deux, les appelants doivent inférer trop de choses à partir du flux de contrôle.

Un Type de Résultat Simple

Vous n'avez pas besoin de théorie des catégories pour bien utiliser cette idée. En TypeScript, une union discriminée suffit :

type Result<T, E> =
  | { ok: true; value: T }
  | { ok: false; error: E };

async function fetchUser(id: string): Promise<Result<User, "NOT_FOUND" | "NETWORK_ERROR">> {
  const response = await fetch(`/api/users/${id}`);

  if (response.status === 404) {
    return { ok: false, error: "NOT_FOUND" };
  }

  if (!response.ok) {
    return { ok: false, error: "NETWORK_ERROR" };
  }

  return { ok: true, value: await response.json() };
}

Maintenant, la signature de la fonction indique à l'appelant que l'échec est attendu et modélisé.

Ce que Vous Gagnez

Cette approche aide lorsque :

  • les modes d'échec font partie de la logique de domaine
  • l'appelant est censé réagir différemment à chacun
  • vous voulez que ces cas soient visibles dans le système de types

Le site d'appel devient explicite :

const result = await fetchUser("123");

if (!result.ok) {
  if (result.error === "NOT_FOUND") return render404();
  return renderRetryState();
}

return renderProfile(result.value);

C'est souvent plus facile à réviser qu'un try/catch qui mélange des erreurs de transport, des erreurs de parsing et des échecs de logique métier dans un seul bloc.

Ce que Vous Devriez Encore Lever

Les types de résultat ne sont pas une raison pour interdire les exceptions.

Lancez une exception lorsque :

  • le processus rencontre une véritable violation d'invariant
  • continuer l'exécution est dangereux
  • une limite de cadre attend des exceptions

Modélisez comme des données lorsque :

  • l'échec est attendu
  • l'appelant doit se ramifier en fonction de cela
  • cela fait partie du comportement normal du produit

Cette distinction est plus utile que "les exceptions sont mauvaises" en tant que règle générale.

Lectures Complémentaires