Aller au contenu

Bonnes pratiques

Les bonnes pratiques JavaScript et TypeScript couvrent les conventions de typage, les idiomes du langage, les anti-patterns a éviter et les stratégies de gestion d'erreurs. Ce chapitre présente des exemples concrets avec explications.


Conventions TypeScript : mode strict

Activer le mode strict dans tsconfig.json est la première décision a prendre. Il active un ensemble de verifications supplémentaires qui previennent des bugs courants.

// tsconfig.json — flags actives par "strict": true
{
  "compilerOptions": {
    "strict": true,
    // Equivalent a activer explicitement :
    // "strictNullChecks": true,       -- null/undefined sont des types distincts
    // "strictFunctionTypes": true,    -- variance stricte sur les fonctions
    // "strictBindCallApply": true,    -- types corrects pour bind/call/apply
    // "strictPropertyInitialization": true, -- proprietes de classe initialisees
    // "noImplicitAny": true,          -- interdiction du any implicite
    // "noImplicitThis": true,         -- this doit etre type
    // "alwaysStrict": true,           -- "use strict" dans les fichiers generes

    // Recommandes en plus de strict
    "noUncheckedIndexedAccess": true,  // arr[0] peut etre T | undefined
    "exactOptionalPropertyTypes": true // { a?: string } != { a?: string | undefined }
  }
}

Éviter any — alternatives

// MAL : any desactive la verification de types
function processData(data: any) {
  return data.name.toUpperCase(); // aucune protection
}

// BIEN : unknown force la verification avant utilisation
function processData(data: unknown): string {
  if (typeof data === 'object' && data !== null && 'name' in data) {
    const { name } = data as { name: unknown };
    if (typeof name === 'string') {
      return name.toUpperCase();
    }
  }
  throw new Error('Donnees invalides');
}

// BIEN : generiques pour la flexibilite avec la securite
function identity<T>(value: T): T {
  return value;
}

// BIEN : types d'utilitaire au lieu de any
type JsonValue = string | number | boolean | null | JsonObject | JsonArray;
type JsonObject = { [key: string]: JsonValue };
type JsonArray = JsonValue[];

Unions discriminees

Les unions discriminees permettent à TypeScript d'inferer le type exact dans chaque branche.

// Union discriminee : chaque variante a un champ "type" unique
type ApiResult<T> =
  | { type: 'success'; data: T }
  | { type: 'error'; message: string; code: number }
  | { type: 'loading' };

function renderResult<T>(result: ApiResult<T>) {
  switch (result.type) {
    case 'success':
      // TypeScript sait que result.data existe ici
      return `Donnees : ${JSON.stringify(result.data)}`;
    case 'error':
      // TypeScript sait que result.message et result.code existent
      return `Erreur ${result.code} : ${result.message}`;
    case 'loading':
      return 'Chargement...';
    // TypeScript signale si un cas n'est pas couvert (exhaustivite)
  }
}

// Pattern d'exhaustivite explicite
function assertNever(value: never): never {
  throw new Error(`Cas non couvert : ${JSON.stringify(value)}`);
}

Idiomes modernes

Optional chaining et nullish coalescing

// Optional chaining ?. — evite les TypeError sur null/undefined
const user = { profile: { address: { city: 'Paris' } } } as {
  profile?: { address?: { city?: string } };
};

// MAL : verbeux et fragile
const city = user && user.profile && user.profile.address && user.profile.address.city;

// BIEN : optional chaining
const city2 = user?.profile?.address?.city; // 'Paris' ou undefined

// Optional chaining avec methodes et indexe
const firstItem = items?.[0]; // undefined si items est null/undefined
const name = obj?.getName?.(); // undefined si getName n'existe pas

// Nullish coalescing ?? — defaut uniquement si null ou undefined
// MAL : || traite 0 et '' comme falsy
const count = userCount || 10; // bug si userCount = 0

// BIEN : ?? ne remplace que null et undefined
const count2 = userCount ?? 10; // 10 seulement si userCount est null/undefined

// Logical assignment
let config = null;
config ??= { timeout: 5000 }; // assigne seulement si null/undefined

Destructuring et spread

// Destructuring avec valeurs par defaut
const { name, role = 'viewer', ...rest } = user;

// Destructuring dans les parametres de fonction
function greet({ name, age = 0 }: { name: string; age?: number }) {
  return `Bonjour ${name}, ${age} ans`;
}

// Spread pour la fusion immuable d'objets
const updatedUser = { ...user, role: 'admin' }; // user inchange

// Spread de tableaux
const allItems = [...existingItems, newItem];
const [first, second, ...remaining] = items;

Template literals et tagged templates

// Template literals pour les chaines complexes
const query = `
  SELECT *
  FROM items
  WHERE user_id = ${userId}
  AND done = ${done}
`;

// Tagged template pour SQL securise (evite les injections)
// Exemple avec sql-template-tag ou similaire
function sql(strings: TemplateStringsArray, ...values: unknown[]): { text: string; values: unknown[] } {
  const text = strings.reduce((acc, str, i) => `${acc}$${i}${str}`);
  return { text, values };
}

const { text, values } = sql`SELECT * FROM items WHERE id = ${itemId}`;

Anti-patterns

Callback hell

// MAL : pyramide de callbacks
getData(id, (err, data) => {
  if (err) { handleError(err); return; }
  processData(data, (err2, result) => {
    if (err2) { handleError(err2); return; }
    saveResult(result, (err3) => {
      if (err3) { handleError(err3); return; }
      console.log('Termine');
    });
  });
});

// BIEN : async/await avec gestion d'erreurs propre
async function pipeline(id: string): Promise<void> {
  const data = await getData(id);
  const result = await processData(data);
  await saveResult(result);
  console.log('Termine');
}

Mutation implicite

// MAL : mutation directe du tableau/objet
function addItem(items: Item[], item: Item) {
  items.push(item); // modifie le tableau original — effet de bord cache
  return items;
}

// BIEN : retourne un nouveau tableau
function addItem(items: Item[], item: Item): Item[] {
  return [...items, item];
}

// BIEN : ReadonlyArray empeche la mutation accidentelle
function processItems(items: ReadonlyArray<Item>): string[] {
  return items.map((i) => i.name); // map retourne un nouveau tableau
}

Promesses non gérées

// MAL : promesse non attendue — les erreurs sont silencieuses
app.get('/items', (req, res) => {
  fetchItems().then((items) => res.json(items)); // pas de catch
});

// BIEN : toujours gerer les erreurs des promesses
app.get('/items', async (req, res) => {
  try {
    const items = await fetchItems();
    res.json(items);
  } catch (error) {
    res.status(500).json({ error: 'Erreur serveur' });
  }
});

// BIEN : node:--unhandled-rejections=strict en production
// et listener global pour les cas restants
process.on('unhandledRejection', (reason) => {
  console.error('Promesse non geree :', reason);
  process.exit(1);
});

Gestion d'erreurs

Classes d'erreurs personnalisees

// src/errors.ts — Hierarchie d'erreurs typees
export class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number = 500,
  ) {
    super(message);
    this.name = this.constructor.name;
    // Fix pour les sous-classes en TypeScript avec target < ES2022
    Object.setPrototypeOf(this, new.target.prototype);
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string, id: number | string) {
    super(`${resource} avec l'id ${id} non trouve`, 'NOT_FOUND', 404);
  }
}

export class ValidationError extends AppError {
  constructor(
    message: string,
    public readonly fields?: Record<string, string[]>,
  ) {
    super(message, 'VALIDATION_ERROR', 400);
  }
}

// Usage dans le code
async function getItem(id: number) {
  const item = await prisma.item.findUnique({ where: { id } });
  if (!item) throw new NotFoundError('Item', id);
  return item;
}

Pattern Result (sans exceptions)

// Pattern Result : alternative aux exceptions pour les erreurs attendues
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

// Fonctions utilitaires
const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
const err = <E>(error: E): Result<never, E> => ({ ok: false, error });

// Usage
async function parseConfig(path: string): Promise<Result<Config, string>> {
  try {
    const content = await fs.readFile(path, 'utf-8');
    const config = JSON.parse(content) as Config;
    return ok(config);
  } catch (e) {
    return err(`Echec de lecture du fichier de config : ${path}`);
  }
}

// Consommation sans try/catch
const result = await parseConfig('./config.json');
if (!result.ok) {
  console.error(result.error);
  process.exit(1);
}
const config = result.value; // TypeScript sait que c'est Config ici

Performance

Tree-shaking

// MAL : import de tout lodash (70 KB)
import _ from 'lodash';
const doubled = _.map([1, 2, 3], (x) => x * 2);

// BIEN : import specifique (tree-shakeable)
import map from 'lodash-es/map.js';
const doubled = map([1, 2, 3], (x) => x * 2);

// MIEUX : methodes natives ES2024+ couvrent 95% des cas lodash
const doubled2 = [1, 2, 3].map((x) => x * 2);

Lazy loading

// Chargement conditionnel de modules lourds
async function generateReport(format: 'pdf' | 'csv') {
  if (format === 'pdf') {
    // Le module pdf n'est charge qu'a la demande
    const { generatePdf } = await import('./pdf-generator.js');
    return generatePdf();
  }
  const { generateCsv } = await import('./csv-generator.js');
  return generateCsv();
}

// React : lazy loading de composants
import { lazy, Suspense } from 'react';

const HeavyChart = lazy(() => import('./HeavyChart.js'));

function Dashboard() {
  return (
    <Suspense fallback={<div>Chargement du graphique...</div>}>
      <HeavyChart data={data} />
    </Suspense>
  );
}

Memoization

// useMemo et useCallback pour eviter les recalculs couteux
import { useMemo, useCallback } from 'react';

function ItemStats({ items }: { items: Item[] }) {
  // Recalcule seulement quand items change
  const stats = useMemo(() => ({
    total: items.length,
    done: items.filter((i) => i.done).length,
    pending: items.filter((i) => !i.done).length,
  }), [items]);

  // Stabilise la reference de la fonction entre les rendus
  const handleToggle = useCallback((id: number) => {
    // logique de toggle
  }, []); // pas de deps : fonction stable

  return (
    <div>
      <p>Total : {stats.total}  Faits : {stats.done}  En cours : {stats.pending}</p>
    </div>
  );
}

Pas de premature optimization

Ne pas ajouter useMemo et useCallback partout. Ces optimisations ont un coût (mémoire, complexité). Les utiliser uniquement quand le profiling confirme un problème de performance, ou pour des calculs clairement coûteux sur de grandes collections.