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
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
neverattrape 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
ifsuffit
Article précédent : 02 - Fonctions
Article suivant : 04 - Commentaires et documentation
Sources
- Martin Fowler, "Replace Nested Conditional with Guard Clauses" - https://refactoring.com/catalog/replaceNestedConditionalWithGuardClauses.html
- TypeScript Handbook, Narrowing and Exhaustiveness Checking - https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking
- Alistair Cockburn, "The Guard Clause Pattern" - https://wiki.c2.com/?GuardClause