Clean code et refactoring - 03 - Conditions et lisibilité : sortir de la pyramide

Guard clauses, ternaires, switch exhaustif, lookup objects : toutes les techniques pour écrire des conditions lisibles en TypeScript.

  1. 01 Clean code et refactoring - 00 - Pourquoi le clean code est un investissement, pas un luxe
  2. 02 Clean code et refactoring - 01 - Nommage : la competence la plus sous-estimee
  3. 03 Clean code et refactoring - 02 - Fonctions : courtes, claires, responsables
  4. 04 Clean code et refactoring - 03 - Conditions et lisibilité : sortir de la pyramide
  5. 05 Clean code et refactoring - 04 - Commentaires et documentation : quand le code ne suffit pas
  6. 06 Clean code et refactoring - 05 - Immutabilite et effets de bord : moins de surprises, moins de bugs
  7. 07 Clean code et refactoring - 06 - Gestion des erreurs propre : fail fast, fail loud
  8. 08 Clean code et refactoring - 07 - Programmation defensive vs offensive : valider aux frontieres, faire confiance a l'intérieur
  9. 09 Clean code et refactoring - 08 - SOLID en pratique avec TypeScript
  10. 10 Clean code et refactoring - 09 - DRY, KISS, YAGNI
  11. 11 Clean code et refactoring - 10 - Couplage et cohesion
  12. 12 Clean code et refactoring - 11 - Complexite cyclomatique
  13. 13 Clean code et refactoring - 12 - Abstractions prematurees vs tardives
  14. 14 Clean code et refactoring - 13 - Code smells
  15. 15 Clean code et refactoring - 14 - Techniques de refactoring
  16. 16 Clean code et refactoring - 15 - Refactoring legacy sans tout casser
  17. 17 Clean code et refactoring - 16 - Tests comme filet de sécurité pour le refactoring
  18. 18 Clean code et refactoring - 17 - Structurer un projet — feature-based vs layer-based
  19. 19 Clean code et refactoring - 18 - Constantes, configuration et magic numbers
  20. 20 Clean code et refactoring - 19 - Linting et formatting — ESLint, Biome, automatiser la qualité
  21. 21 Clean code et refactoring - 20 - Conventions d'équipe et ADR
  22. 22 Clean code et refactoring - 21 - Dette technique — quand elle est acceptable, quand elle tue le projet
  23. 23 Clean code et refactoring - 22 - Code review — donner et recevoir du feedback
  24. 24 Clean code et refactoring - 23 - Glossaire — tous les termes de la serie

03 - Conditions et lisibilité : sortir de la pyramide

Ce que tu vas apprendre

  • Pourquoi les conditions imbriquees tuent la lisibilité
  • Le pattern guard clause pour aplatir le code
  • Quand utiliser (et ne pas utiliser) les ternaires
  • Le switch exhaustif en TypeScript
  • Les lookup objects comme alternative aux chaînes de if

Prerequisites

02 - Fonctions


J'ai passe un entretien technique il y a quelques annees ou le candidat devait écrire une fonction de calcul de prix avec des remises. Le gars etait competent, il connaissait TypeScript, il avait de bonnes idees. Mais son code ressemblait a ca :

typescriptfunction getPrice(user: User, product: Product, coupon?: Coupon) {
  if (user) {
    if (product) {
      if (product.isAvailable) {
        if (user.isPremium) {
          if (coupon) {
            if (coupon.isValid) {
              if (coupon.appliesTo(product.category)) {
                return product.price * 0.7 * (1 - coupon.discount);
              } else {
                return product.price * 0.7;
              }
            } else {
              return product.price * 0.7;
            }
          } else {
            return product.price * 0.7;
          }
        } else {
          if (coupon) {
            if (coupon.isValid) {
              return product.price * (1 - coupon.discount);
            }
          }
          return product.price;
        }
      }
    }
  }
  return 0;
}

Sept niveaux d'imbrication. Du return product.price * 0.7 duplique trois fois. Un return 0 silencieux tout en bas qui cache les cas d'erreur. Ce n'est pas du mauvais code par incompetence. C'est du mauvais code par accumulation : chaque if a ete ajoute un par un, et personne n'a pris le recul nécessaire.

La pyramide du malheur

Le cerveau humain est mauvais pour traquer les branches imbriquees. A chaque niveau d'imbrication, tu dois te souvenir de toutes les conditions parentes. Au-dela de 3 niveaux, c'est la surcharge cognitive.

La regle : 2 niveaux d'imbrication maximum. Si tu depasses, c'est un signal de refactoring.

Guard clauses : evacuer les cas speciaux

On en a parle dans l'article sur les fonctions. Le principe : gerer les cas d'erreur et les cas limites en haut de la fonction, puis laisser le happy path couler sans imbrication.

Reprenons le calcul de prix :

typescriptfunction getPrice(user: User, product: Product, coupon?: Coupon): number {
  if (!user) throw new Error("User required");
  if (!product) throw new Error("Product required");
  if (!product.isAvailable) return 0;

  const basePrice = product.price;
  const premiumMultiplier = user.isPremium ? 0.7 : 1;
  const couponDiscount = getValidCouponDiscount(coupon, product.category);

  return basePrice * premiumMultiplier * (1 - couponDiscount);
}

function getValidCouponDiscount(
  coupon: Coupon | undefined,
  category: string
): number {
  if (!coupon) return 0;
  if (!coupon.isValid) return 0;
  if (!coupon.appliesTo(category)) return 0;
  return coupon.discount;
}

Zero imbrication. La logique tient en une formule. Les cas limites sont traites par une fonction dédiée.

Ternaires : une seule ligne ou rien

Le ternaire est fait pour les choix simples sur une ligne :

typescript// Bon - court et clair
const label = isActive ? "Actif" : "Inactif";
const tax = country === "FR" ? 0.2 : 0;
const greeting = `Bonjour ${user.name ?? "visiteur"}`;

Le ternaire n'est pas fait pour la logique complexe :

typescript// Mauvais - illisible
const price =
  user.isPremium
    ? product.category === "electronics"
      ? product.price * 0.8
      : product.price * 0.9
    : coupon
      ? coupon.isValid
        ? product.price * (1 - coupon.discount)
        : product.price
      : product.price;

Si ton ternaire dépassé une ligne, utilise un if ou extrais une fonction. Pas de discussion.

Éviter les conditions negatives

Le cerveau traite plus lentement les negations. Double negation ? Triple ? Impossible.

typescript// Mauvais - negation confuse
if (!user.isNotVerified) { ... }
if (!isDisabled && !isHidden) { ... }
if (!(items.length === 0)) { ... }

// Bon - formulation positive
if (user.isVerified) { ... }
if (isEnabled && isVisible) { ... }
if (items.length > 0) { ... }

Quand tu as un booleen negatif (isDisabled, isHidden), demande-toi si le booleen positif ne serait pas plus clair. isEnabled est presque toujours meilleur que !isDisabled.

Simplification par algebre booleenne

Parfois, une condition complexe se simplifie :

typescript// Avant - condition redondante
if (isAdmin || (isEditor && isAdmin)) {
  // ...
}

// Apres - isAdmin absorbe le second terme
if (isAdmin || isEditor) {
  // ...
}

Les lois de De Morgan sont utiles aussi :

typescript// Avant
if (!(isActive && hasPermission)) { ... }

// Apres (De Morgan : !(A && B) === !A || !B)
if (!isActive || !hasPermission) { ... }

La seconde forme est plus facile a lire : "si l'utilisateur n'est pas actif OU n'a pas la permission".

Switch exhaustif en TypeScript

Un switch classique a un problème : tu peux oublier un cas. TypeScript peut t'aider.

typescripttype OrderStatus = "pending" | "confirmed" | "shipped" | "delivered";

function getStatusLabel(status: OrderStatus): string {
  switch (status) {
    case "pending":
      return "En attente";
    case "confirmed":
      return "Confirmee";
    case "shipped":
      return "Expediee";
    case "delivered":
      return "Livree";
    default: {
      const _exhaustive: never = status;
      throw new Error(`Status inconnu: ${_exhaustive}`);
    }
  }
}

Le never dans le default fait que TypeScript te crie dessus si tu ajoutes un nouveau status sans gerer le cas dans le switch. C'est un filet de sécurité a la compilation. Si tu ajoutes "cancelled" au type OrderStatus, le code ne compile plus tant que tu n'as pas ajoute le case.

J'utilise cette technique dans tous mes projets, y compris ceux dont je parle sur paltemps.fr. C'est gratuit et ca évité des bugs.

Lookup objects : l'alternative elegante aux chaînes de `if`

Quand tu as une serie de if qui associent des valeurs a des clés, un objet est souvent plus clair :

typescript// Chaine de if
function getErrorMessage(code: string): string {
  if (code === "NOT_FOUND") return "Resource introuvable";
  if (code === "UNAUTHORIZED") return "Acces refuse";
  if (code === "FORBIDDEN") return "Action interdite";
  if (code === "VALIDATION") return "Donnees invalides";
  if (code === "RATE_LIMIT") return "Trop de requetes";
  return "Erreur inconnue";
}

// Lookup object
const ERROR_MESSAGES: Record<string, string> = {
  NOT_FOUND: "Resource introuvable",
  UNAUTHORIZED: "Acces refuse",
  FORBIDDEN: "Action interdite",
  VALIDATION: "Donnees invalides",
  RATE_LIMIT: "Trop de requetes",
};

function getErrorMessage(code: string): string {
  return ERROR_MESSAGES[code] ?? "Erreur inconnue";
}

Le lookup object a trois avantages : il est plus compact, il est plus facile a etendre (ajouter une ligne vs ajouter un if), et il est serialisable (tu peux le charger depuis un fichier de config).

Ca marche aussi pour les fonctions :

typescripttype Action = "create" | "update" | "delete";

const handlers: Record<Action, (id: string) => Promise<void>> = {
  create: async (id) => { /* ... */ },
  update: async (id) => { /* ... */ },
  delete: async (id) => { /* ... */ },
};

async function handleAction(action: Action, id: string): Promise<void> {
  const handler = handlers[action];
  await handler(id);
}

Pas de switch. Pas de if. Pas de default oublie. TypeScript vérifié que toutes les actions sont gerees.

Quand garder un `if` simple

Ne transforme pas un if de 3 lignes en lookup object par principe. Si la condition est simple et qu'il n'y a que 2 branches, un if est parfait :

typescript// C'est tres bien comme ca
if (user.isPremium) {
  applyPremiumDiscount(order);
}

Le refactoring de conditions a du sens quand : il y a plus de 3 branches, il y a de l'imbrication, ou la logique est dupliquee. Pour le reste, la simplicité gagne.

Résumé

  • Maximum 2 niveaux d'imbrication dans une fonction
  • Les guard clauses evacuent les cas speciaux en haut
  • Les ternaires : une seule ligne, pas plus
  • Prefere les formulations positives aux negations
  • Le switch exhaustif avec never attrape les cas oublies a la compilation
  • Les lookup objects remplacent les longues chaînes de if/switch
  • Ne refactorise pas les conditions simples : 2 branches, un if suffit

Article précédent : 02 - Fonctions

Article suivant : 04 - Commentaires et documentation

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.