À la une

4 outils pour préserver la qualité de votre code

La qualité du code ne chute jamais brutalement. Elle s'érode petit à petit, commit après commit, jusqu'à ce que certains fichiers deviennent des zones interdites, synonymes de bugs à répétition.

TechWebDX

Après plusieurs dizaines de projets lancés, j'ai pu faire un constat sans appel : plus on attend pour mettre en place des règles de qualité strictes, plus la dette technique s'accumule. Et personne n'a jamais le temps ou l'envie de la traiter après coup.

La qualité du code ne chute jamais brutalement. Elle s’érode petit à petit, commit après commit, jusqu’à ce que certains fichiers deviennent des zones interdites, synonymes de bugs à répétition.

Aujourd'hui, je vous partage quelques conseils éprouvés qui vous permettront de maintenir une qualité de code robuste et homogène dès le premier jour de votre projet tout en faisant progresser votre équipe.

Quels outils pour contrôler la qualité du code ?

1. TypeScript : Le guide infaillible

⚡ TL;DR : Détecte les erreurs avant l'exécution et rend le code auto-documenté.

TypeScript transforme l'approche du développement en déplaçant la détection d'erreurs de l'exécution vers la compilation. Le strict mode, activé via "strict": true, regroupe plusieurs vérifications cruciales : noImplicitAny (interdit les types any implicites), strictNullChecks (force la gestion de null/undefined) et strictFunctionTypes (renforce la compatibilité des signatures). Cette configuration élimine une grande partie des erreurs runtime classiques et améliore considérablement l'expérience de développement.

Voici les règles préliminaires que j'ajoute par défaut dans TSConfig :

// tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitOverride": true
  }
}

Par exemple, noUncheckedIndexedAccess est une de mes règles préférées. Elle m'oblige à gérer les cas où array[index] peut être undefined, éliminant ces terribles erreurs runtime Cannot read property of undefined.

// ❌ Sans noUncheckedIndexedAccess
const user = users[0];
user.name; // Crash si users est vide
 
// ✅ Avec noUncheckedIndexedAccess
const user = users[0];
if (user) {
  user.name; // TypeScript force à vérifier l'existence de user
}

Autre exemple avec la règle exactOptionalPropertyTypes qui permet de casser l'ambiguïté des propriétés optionnelles. Sans cette règle, TypeScript ne fait pas la différence entre une propriété absente et une propriété undefined.

// ❌ Sans exactOptionalPropertyTypes
interface User {
  name?: string; // équivaut à name: string | undefined
}
 
const user: User = { name: undefined };
 
// ✅ Avec exactOptionalPropertyTypes
interface User {
  name?: string; // optionnel, si défini alors string uniquement
}
 
const user: User = {}; // `name` ne peut pas être défini à undefined

Ce que j'ai compris :

💡 TypeScript ne fait pas qu'éviter les bugs : il allège ma charge cognitive en rendant le code explicite et prévisible.

2. ESLint : Le sensei de l'équipe

⚡ TL;DR : Automatise les bonnes pratiques et élève le niveau de toute l'équipe.

J'ai longtemps pensé qu'ESLint servait uniquement à détecter les erreurs et à bien écrire le code. En réalité, il a un autre véritable pouvoir : réduire la friction en équipe en imposant des standards homogènes. Cet outil me fait gagner un temps précieux lors des relectures de code : au lieu de corriger manuellement les patterns dangereux et les incohérences, je peux me concentrer sur l'architecture et la logique métier.

// eslint.config.mts
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
 
export default tseslint.config(
  {
    ignores: ['node_modules/**', 'public/**', '**/build/**', '**/dist/**', '**/coverage/**'],
  },
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  {
    files: ['**/*.{js,jsx,ts,tsx}'],
    rules: {
      // ...quelques règles en exemple
      'no-console': 'error',
      'max-params': ['error', 3],
      'func-style': ['error', 'declaration'],
      'prefer-arrow-callback': 'error',
      'react/jsx-no-literals': 'error',
    },
  },
  {
    files: ['**/*.ts', '**/*.tsx'],
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
    rules: {
      // ...quelques règles en exemple
      '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
      '@typescript-eslint/switch-exhaustiveness-check': 'error',
      '@typescript-eslint/strict-boolean-expressions': 'error',
      '@typescript-eslint/explicit-module-boundary-types': 'off',
    },
  }
);

Prenons un exemple concret avec 'max-params': ['error', 3]. Combien de fois avez-vous vu (ou écrit) :

// ❌ 8 paramètres : ordre à retenir et difficile à maintenir
function createUser(name, email, age, role, department, isActive, createdById, permissions) {
  // Bug garanti dès qu'on inverse deux paramètres de même type
}
 
// ✅ 2 paramètres : design orienté objet et évolutif
function createUser(user, options) {
  const { name, email, age, role, department, isActive, createdById } = user;
  const { permissions } = options;
  // Auto-complétion, ordre libre, extensibilité native
}

Autre exemple avec @typescript-eslint/switch-exhaustiveness-check :

type Status = 'pending' | 'approved' | 'rejected';
 
function handleStatus(status: Status) {
  switch (status) {
    case 'pending':
      return 'En attente';
    case 'approved':
      return 'Approuvé';
    // ❌ ESLint détecte qu'il manque le cas 'rejected'
    // Error: Switch is not exhaustive. Cases not matched: 'rejected'
  }
}

Ou encore simplement 'no-console': 'error' qui empêche les fuites accidentelles de données sensibles via les logs de debug et qui retire cette impression de code "non-fini".

Ce que j'ai compris :

💡 ESLint ne corrige pas seulement le code qui risque d'être défectueux : il élève le niveau de toute l'équipe en automatisant les bonnes pratiques.

3. Prettier : Le pacificateur de l'équipe

⚡ TL;DR : Élimine les débats de formatage et accélère les code reviews.

// prettier.config.mjs
/**
 * @see https://prettier.io/docs/configuration
 * @type {import("prettier").Config}
 */
const config = {
  plugins: ['prettier-plugin-tailwindcss'],
  singleQuote: true,
  trailingComma: 'es5',
  tabWidth: 2,
  printWidth: 100,
};
 
export default config;

Au lieu de débattre sur "est-ce que cette PR est bien formatée ?", l'équipe se concentre sur "est-ce que cette PR résout vraiment le problème ?". Prettier permet de garantir que le code respecte le même style d'écriture sur tous les IDEs de l'équipe. Quand Bob modifie un fichier d'Alice, le formatage reste identique : même indentation, mêmes guillemets, mêmes virgules finales... Le diff de la revue de code ne révèle que les vraies modifications de Bob, sans parasitage stylistique.

Ce que j'ai compris :

💡 Prettier n'élimine pas seulement les discussions stériles : il libère l'énergie mentale pour ce qui apporte vraiment de la valeur (l'architecture, la logique métier, les effets de bord).

4. Husky : Le garde du corps du repository

⚡ TL;DR : Bloque les erreurs avant qu'elles atteignent Git.

# .husky/pre-commit
#!/bin/bash
 
lint-staged

Combien de fois le build a été cassé dans la CI... sans que personne ne s'en aperçoive avant la revue de code ? Combien de commits mal formatés auraient pu être évités avec commitlint ? Combien de "fix: typo" auraient pu être interceptés automatiquement avec codespell ?

Husky est mon assurance qualité en local. En exploitant les hooks Git, il transforme les erreurs humaines en impossibilités techniques. Au lieu de découvrir les problèmes en CI/CD (après souvent plusieurs minutes d'attente), ou pire, en production, Husky les intercepte instantanément sur votre machine.

Sans Husky ❌

$ git commit -m "feat: new order management"
 Commit réussi
 
# 15 minutes plus tard en CI...
 Build failed: TypeScript errors
 Tests failed: 3 tests broken
 Linting failed: console.log detected
 
# = 3 allers-retours, 45 minutes perdues

Avec Husky ✅

$ git commit -m "feat: new order management"
 Running pre-commit hooks...
 
 Type checking failed:
  src/user.ts:42:5 - error TS2322: Type 'string' is not assignable to type 'number'
 
 ESLint found issues:
  src/api.ts:15:9 - 'console.log' not allowed (no-console)
 
 3 tests failed:
  UserService should validate email format
 
 Commit blocked - fix errors and try again
 
# = Erreur détectée en 3 secondes, contexte encore frais

Exemple de configuration :

# .husky/pre-commit
npx lint-staged
// lint-staged.config.mts
import { type Configuration } from 'lint-staged';
 
const config: Configuration = {
  'package.json': ['npx sort-package-json'],
  '*.{ts,tsx}': [
    'eslint --fix --max-warnings 0',
    'prettier --write',
    () => 'tsc --noEmit --skipLibCheck',
  ],
  '*.md': ['prettier --write'],
  '*.json': ['prettier --write'],
};
 
export default config;

Un des usages que j'apprécie particulièrement est la validation automatique des contrats d'API. Grâce à des outils comme OpenAPI TypeScript, Husky peut s'assurer que les types générés depuis les spécifications OpenAPI sont à jour et que l'interface entre le frontend et le backend reste cohérente.

Cette approche permet de détecter immédiatement les breaking changes d'API et les désynchronisations entre les équipes frontend/backend, avant même que le code atteigne la CI.

Ce que j'ai compris :

💡 Husky ne bloque pas seulement l'apparition d'un défaut : il guide le développeur à corriger le défaut détecté au plus tôt, pendant que le contexte est encore frais dans sa mémoire.

Pour démarrer

Si vous commencez un nouveau projet, voici le kit de survie :

# Installation en une commande
yarn add -D \
  typescript \
  eslint \
  typescript-eslint \
  @eslint/js \
  prettier \
  eslint-config-prettier \
  husky \
  lint-staged
 
# Pour Next.js, ajouter aussi :
yarn add -D \
  eslint-config-next \
  @eslint/eslintrc
 
# Initialisation
npx husky init
npx tsc --init --strict

L'effet domino de la qualité

Ces quatre outils forment une chaîne de transformation continue avant le déploiement du code :

flowchart LR
    Dev((Dev)) --> TS[🛡️ SÉCURISER<br/>TypeScript]
    TS --> ESLint[📚 FORMER<br/>ESLint]
    ESLint --> Prettier[🎨 HARMONISER<br/>Prettier]
    Prettier --> Husky[✅ VALIDER<br/>Husky]
    Husky --> Git((Git))
 
    style TS fill:#dbeafe,stroke:#3b82f6
    style ESLint fill:#fef3c7,stroke:#f59e0b
    style Prettier fill:#ede9fe,stroke:#8b5cf6
    style Husky fill:#d1fae5,stroke:#10b981

Chaque étape renforce la précédente :

  1. TypeScript détecte les erreurs pendant que vous codez en garantissant la cohérence des types
  2. ESLint impose les bonnes pratiques en élèvant progressivement le niveau de l'équipe
  3. Prettier harmonise le style en éliminant les débats sur le formatage
  4. Husky bloque les erreurs avant qu'elles n'atteignent Git en automatisant les vérifications

Le résultat ? Une équipe qui progresse naturellement, où chaque développeur améliore progressivement non seulement le code mais aussi ses compétences. Ces outils ne se contentent pas de corriger : ils transforment la culture de développement en créant un cercle vertueux d'amélioration continue.

Impact mesurable sur vos projets

Après avoir mis en place et bien configurés ces outils sur plusieurs projets, voici les améliorations constatées :

  • Temps de debug diminué : les types attrapent les erreurs avant l'exécution
  • Code reviews plus rapides : focus sur la logique, pas le style
  • Bugs en production diminués : la majorité interceptée avant le commit
  • Onboarding plus rapide : les règles guident les nouveaux développeurs
  • Dette technique limitée : qualité maintenue dès le jour 1

Ces outils doivent être configurés avec des règles strictes dès le départ. La tentation de "commencer souple pour ne pas bloquer l'équipe" est un piège : vous accumulerez de la dette technique que personne ne voudra corriger plus tard.

Mieux vaut une semaine d'adaptation douloureuse qu'un an de code médiocre immaintenable.

L'important est de les mettre en place dès les premiers commits (il est 100x plus difficile de les imposer sur une base de code existante). Les besoins évoluent avec le temps, de nouvelles règles ESLint apparaissent, TypeScript ajoute de nouvelles options strictes. Prenez une heure par mois pour revoir votre configuration, ce ne sera pas cher payé.