Bonnes pratiques#

Hide code cell source

import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

Maîtriser la syntaxe de TypeScript est une première étape ; savoir l’utiliser avec discernement en est une autre. Ce dernier chapitre rassemble les principes, les patterns et les stratégies qui distinguent une base de code TypeScript robuste d’une base de code qui accumule les any et les assertions de types. Ces bonnes pratiques sont le fruit de l’expérience collective de la communauté TypeScript et des équipes qui maintiennent des projets à grande échelle.

Principes généraux#

Préférer l’inférence#

Le compilateur TypeScript possède un moteur d’inférence remarquablement puissant. Il est souvent inutile d’annoter explicitement les types quand le compilateur peut les déduire :

// Redondant : le type string est évident
const prénom: string = 'Alice';

// Correct : l'inférence est suffisante
const prénom = 'Alice'; // TypeScript infère string

// Redondant : le type de retour est évident depuis les types des paramètres
function additionner(a: number, b: number): number {
  return a + b;
}

// Correct : le type de retour number est inféré automatiquement
function additionner(a: number, b: number) {
  return a + b;
}

Annoter aux frontières#

En revanche, les frontières méritent des annotations explicites : les signatures de fonctions exportées, les paramètres de composants, les valeurs retournées par des fonctions publiques d’une bibliothèque. Ces annotations servent de contrat, de documentation vivante, et elles empêchent les types inférés d’une implémentation interne de « fuir » vers l’extérieur.

// Frontière d'un module : annotation explicite du type de retour
export function créerToken(utilisateurId: number, durée: number): string {
  // L'implémentation peut changer ; le contrat (string) reste stable
  return jwt.sign({ sub: utilisateurId }, secret, { expiresIn: durée });
}

Éviter any#

any désactive la vérification de types pour une valeur. C’est un abandon de la sécurité que TypeScript offre. Les alternatives existent presque toujours :

// À éviter : any annule toute vérification
function traiter(données: any) {
  return données.propriétéInconnue; // TypeScript ne détecte aucune erreur
}

// Préférer unknown : force une vérification avant l'utilisation
function traiter(données: unknown) {
  if (typeof données === 'object' && données !== null && 'nom' in données) {
    return (données as { nom: string }).nom;
  }
  throw new Error('Format de données invalide');
}

// Ou utiliser un schéma Zod pour valider et typer en une seule opération
function traiter(données: unknown) {
  const validé = z.object({ nom: z.string() }).parse(données);
  return validé.nom; // Type : string
}

Remarque 37

Quand any semble inévitable — lors d’une migration progressive, pour intégrer une bibliothèque non typée, ou temporairement pour débloquer une situation complexe — il est préférable de l’isoler dans un adaptateur ou un fichier dédié, et de marquer le code avec un commentaire // TODO: supprimer any. L’objectif est de ne jamais laisser any se propager silencieusement à travers la base de code.

Structuration des types#

Colocaliser les types avec le code#

Un type défini près du code qui l’utilise est plus facile à maintenir qu’un type caché dans un fichier types.ts global :

// types.ts global — difficile à naviguer, crée des dépendances circulaires
export interface Utilisateur { ... }
export interface Commande { ... }
export interface Produit { ... }

// Mieux : coloquer le type avec le module qui en est propriétaire
// utilisateur/types.ts
export interface Utilisateur {
  id: number;
  nom: string;
  email: string;
}

// commande/types.ts
import type { Utilisateur } from '../utilisateur/types';
export interface Commande {
  id: number;
  acheteur: Utilisateur;
  articles: LigneDeCommande[];
}

Nommage : éviter les préfixes et suffixes conventionnels#

Certaines bases de code utilisent des conventions comme le préfixe I pour les interfaces (IUtilisateur) ou le suffixe T pour les types (UtilisateurT), héritées de Java ou de C++. Ces conventions sont découragées dans la communauté TypeScript pour plusieurs raisons :

// Convention I-prefix — à éviter
interface IUtilisateur { nom: string; }
interface IRepository<T> { trouver(id: number): Promise<T>; }

// Convention T-suffix — à éviter
type UtilisateurT = { nom: string; };

// Recommandé : noms descriptifs sans décoration
interface Utilisateur { nom: string; }
interface RépertutilisateurDépôt<T> { trouver(id: number): Promise<T>; }
type Utilisateur = { nom: string; };

Remarque 38

Le guide de style officiel de TypeScript et le dépôt DefinitelyTyped recommandent explicitement de ne pas utiliser le préfixe I pour les interfaces. En TypeScript, la distinction entre interface et type est souvent une question de style plutôt que de sémantique : les deux peuvent être étendus, combinés et utilisés dans des positions équivalentes. Ce qui compte, c’est que le nom décrive clairement la structure ou le comportement représenté.

Gestion des cas limites#

Exhaustivité avec never#

Le type never est le type bottom de TypeScript : aucune valeur n’est de ce type. Il est particulièrement utile pour garantir l’exhaustivité des branches dans les switch ou les chaînes conditionnelles.

type Forme =
  | { type: 'cercle'; rayon: number }
  | { type: 'rectangle'; largeur: number; hauteur: number }
  | { type: 'triangle'; base: number; hauteur: number };

function calculerSurface(forme: Forme): number {
  switch (forme.type) {
    case 'cercle':
      return Math.PI * forme.rayon ** 2;
    case 'rectangle':
      return forme.largeur * forme.hauteur;
    case 'triangle':
      return (forme.base * forme.hauteur) / 2;
    default:
      // Si on ajoute un nouveau type à l'union sans mettre à jour ce switch,
      // TypeScript signale une erreur ici
      return assertJamaisAtteint(forme);
  }
}

function assertJamaisAtteint(valeur: never): never {
  throw new Error(`Cas non géré : ${JSON.stringify(valeur)}`);
}

```{prf:definition} assertNever :label: definition-16-01 La fonction assertNever(x: never): never est un pattern standard en TypeScript pour garantir l’exhaustivité. Elle prend en paramètre une valeur de type never. Si TypeScript peut vérifier statiquement que toutes les branches d’une union ont été traitées, la valeur dans la branche default est effectivement de type never, et l’appel compile sans erreur. Si une branche manque, TypeScript refuse de compiler car il ne peut pas prouver que la valeur est never. L’implémentation lève une erreur à l’exécution pour gérer les cas impossibles en théorie mais potentiels si le code est appelé depuis JavaScript non typé.


## Patrons de conception typés

### Builder pattern fluide

Le pattern builder est naturellement expressif en TypeScript car les méthodes chaînées peuvent être typées pour retourner `this` :

```typescript
class RequêteConstructeur {
  private url: string;
  private méthode: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET';
  private en_têtes: Record<string, string> = {};
  private corps: unknown;

  constructor(url: string) {
    this.url = url;
  }

  avecMéthode(méthode: this['méthode']): this {
    this.méthode = méthode;
    return this;
  }

  avecEn_tête(clé: string, valeur: string): this {
    this.en_têtes[clé] = valeur;
    return this;
  }

  avecCorps(corps: unknown): this {
    this.corps = corps;
    return this;
  }

  async exécuter<T>(): Promise<T> {
    const réponse = await fetch(this.url, {
      method: this.méthode,
      headers: this.en_têtes,
      body: this.corps !== undefined ? JSON.stringify(this.corps) : undefined,
    });
    return réponse.json() as T;
  }
}

// Usage fluide
const utilisateurs = await new RequêteConstructeur('/api/utilisateurs')
  .avecMéthode('POST')
  .avecEn_tête('Content-Type', 'application/json')
  .avecCorps({ nom: 'Alice', email: 'alice@exemple.com' })
  .exécuter<{ id: number }>();

Discriminated union pour les machines à états#

Les unions discriminées sont idéales pour modéliser les états d’une machine à états finis, garantissant que chaque état n’expose que les données qui lui sont pertinentes :

type ÉtatRequête<T> =
  | { statut: 'inactif' }
  | { statut: 'chargement' }
  | { statut: 'succès'; données: T; horodatage: Date }
  | { statut: 'erreur'; message: string; code: number };

function afficherRequête<T>(état: ÉtatRequête<T>): string {
  switch (état.statut) {
    case 'inactif':    return 'En attente…';
    case 'chargement': return 'Chargement en cours…';
    case 'succès':
      // état.données est accessible ici et seulement ici
      return `Succès à ${état.horodatage.toLocaleTimeString()}`;
    case 'erreur':
      // état.message et état.code sont accessibles ici et seulement ici
      return `Erreur ${état.code} : ${état.message}`;
  }
}

Option/Result à la Rust#

TypeScript n’a pas de type Option<T> ou Result<T, E> natif, mais on peut les modéliser élégamment :

type Option<T> = { some: true; valeur: T } | { some: false };
type Result<T, E> = { ok: true; valeur: T } | { ok: false; erreur: E };

function diviser(a: number, b: number): Result<number, string> {
  if (b === 0) {
    return { ok: false, erreur: 'Division par zéro' };
  }
  return { ok: true, valeur: a / b };
}

const résultat = diviser(10, 2);
if (résultat.ok) {
  console.log(résultat.valeur); // Type : number
} else {
  console.error(résultat.erreur); // Type : string
}

Performance du compilateur#

Types trop complexes#

Les types conditionnels profondément imbriqués et les types récursifs peuvent ralentir considérablement le compilateur TypeScript, voire dépasser les limites d’instantiation. Quelques règles pratiques :

// Fragile : type conditionnel profond
type ExtractorProfond<T> = T extends { a: infer A }
  ? A extends { b: infer B }
    ? B extends { c: infer C }
      ? C
      : never
    : never
  : never;

// Préférable : décomposer en types intermédiaires nommés
type ExtraireA<T> = T extends { a: infer A } ? A : never;
type ExtraireB<T> = T extends { b: infer B } ? B : never;
type ExtraireC<T> = T extends { c: infer C } ? C : never;

type ExtractorLisible<T> = ExtraireC<ExtraireB<ExtraireA<T>>>;

@ts-ignore versus @ts-expect-error#

Comme vu au chapitre précédent, @ts-expect-error est toujours préférable à @ts-ignore car il échoue si l’erreur attendue disparaît, servant ainsi de filet de sécurité :

// À éviter : @ts-ignore est silencieux même si l'erreur disparaît
// @ts-ignore
const x: string = 42;

// Préférable : @ts-expect-error échoue si la ligne ne provoque plus d'erreur
// @ts-expect-error — 42 n'est pas un string (attendu pour le test)
const x: string = 42;

Migration de JavaScript vers TypeScript#

Stratégie progressive#

La migration d’une base de code JavaScript existante vers TypeScript est rarement une réécriture complète. La stratégie progressive est la plus sûre :

Étape 1 — @ts-check sans changer l’extension

En ajoutant // @ts-check en tête d’un fichier .js, l’éditeur active la vérification TypeScript via l’inférence JSDoc, sans modifier le fichier :

// @ts-check
/** @type {string} */
const nom = 'Alice';

Étape 2 — allowJs et checkJs dans le tsconfig

{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,
    "strict": false
  }
}

Cela active la vérification de tous les fichiers .js du projet, en mode non-strict pour éviter une avalanche d’erreurs dès le départ.

Étape 3 — Renommer les fichiers prioritaires en .ts

On commence par les modules les plus importants : les utilitaires partagés, les types de données centraux, les couches d’accès aux données. Les fichiers .js restants continuent de fonctionner grâce à allowJs.

Étape 4 — Activer strict progressivement

Une fois les fichiers clés migrés, on active strict: true et on corrige les erreurs module par module.

Étape 5 — Supprimer allowJs

Quand tous les fichiers sont .ts ou .tsx, on supprime allowJs et checkJs du tsconfig.

Exemple 17 (Priorités de migration)

Toutes les parties d’une base de code ne méritent pas le même niveau d’effort de migration. On recommande de prioriser dans cet ordre :

  1. Les interfaces de données (objets, API responses) — à migrer en premier car elles propagent leur typage à tous les consommateurs.

  2. Les fonctions utilitaires partagées — fortement réutilisées, leur typage bénéficie à tout le projet.

  3. Les couches de service et d’accès aux données — frontières critiques avec le monde extérieur.

  4. Les composants UI — bénéficient énormément du typage des props.

  5. Les tests — en dernier, car ils peuvent utiliser any temporairement sans impact sur la production.

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 7))
ax.set_xlim(-0.5, 14.5)
ax.set_ylim(-0.5, 8.0)
ax.axis('off')
ax.set_title(
    'Feuille de route : migration progressive JavaScript → TypeScript',
    fontsize=14, fontweight='bold', pad=16
)

étapes = [
    ('Étape 0\nJS pur', 0, '#e74c3c', 0),
    ('Étape 1\n@ts-check', 15, '#e67e22', 1),
    ('Étape 2\nallowJs +\ncheckJs', 35, '#f1c40f', 2),
    ('Étape 3\nFichiers clés\nen .ts', 60, '#2ecc71', 3),
    ('Étape 4\nstrict: true', 85, '#27ae60', 4),
    ('Étape 5\nTS complet', 100, '#1a8a5a', 5),
]

bénéfices = [
    'Aucune\nvérification',
    'Vérification\nJSDoc basique',
    'Erreurs\nJS détectées',
    'Types\nsolides sur\nles modules clés',
    'Sécurité\ncomplète',
    'Codebase\npleinement\ntypée',
]

x_positions = np.linspace(0.5, 12.5, len(étapes))
y_barres = 3.5
hauteur_barre = 2.5
largeur_barre = 1.7

for i, (label, pct, couleur, _) in enumerate(étapes):
    x = x_positions[i] - largeur_barre / 2

    # Barre de progression
    barre_bg = patches.FancyBboxPatch(
        (x, y_barres), largeur_barre, hauteur_barre,
        boxstyle='round,pad=0.08', linewidth=1.5,
        edgecolor='#cccccc', facecolor='#f5f5f5'
    )
    ax.add_patch(barre_bg)

    h = hauteur_barre * pct / 100
    barre = patches.FancyBboxPatch(
        (x, y_barres), largeur_barre, h,
        boxstyle='round,pad=0.08', linewidth=0,
        edgecolor='none', facecolor=couleur, alpha=0.8
    )
    ax.add_patch(barre)

    # Pourcentage
    ax.text(x_positions[i], y_barres + hauteur_barre + 0.3,
            f'{pct} %', ha='center', va='center',
            fontsize=10, fontweight='bold', color=couleur)

    # Étiquette de l'étape
    ax.text(x_positions[i], y_barres - 0.55, label,
            ha='center', va='center', fontsize=8.5,
            fontweight='bold', color='#2c3e50')

    # Bénéfice
    ax.text(x_positions[i], y_barres - 1.8, bénéfices[i],
            ha='center', va='center', fontsize=7.5,
            color='#555555', style='italic')

    # Flèche entre étapes
    if i < len(étapes) - 1:
        x_flèche = x_positions[i] + largeur_barre / 2 + 0.05
        ax.annotate('',
            xy=(x_positions[i + 1] - largeur_barre / 2 - 0.05,
                y_barres + hauteur_barre / 2),
            xytext=(x_flèche, y_barres + hauteur_barre / 2),
            arrowprops=dict(arrowstyle='->', color='#888888', lw=1.5))

# Axe vertical "Niveau de typage"
ax.text(-0.3, y_barres + hauteur_barre / 2, 'Niveau de\ntypage',
        ha='center', va='center', fontsize=9, color='#555555',
        rotation=90)
ax.annotate('',
    xy=(-0.1, y_barres + hauteur_barre),
    xytext=(-0.1, y_barres),
    arrowprops=dict(arrowstyle='->', color='#888888', lw=1.5))

# Titre de l'axe X
ax.text(6.5, 0.1, 'Progression temporelle de la migration',
        ha='center', va='center', fontsize=9, color='#555555')

plt.tight_layout()
plt.show()
_images/18d279100984697d631a2851514f4dcc5c1d69a7d751395cec0106af4ab7af69.png

Ressources et aller plus loin#

L’apprentissage de TypeScript ne s’arrête pas à ce livre. Voici les ressources les plus précieuses pour approfondir sa maîtrise.

Documentation et références

  • Documentation officielle TypeScript (typescriptlang.org) : le Handbook couvre tous les concepts avec des exemples ; le What’s New de chaque version est indispensable.

  • TypeScript Deep Dive de Basarat Ali Syed (basarat.gitbook.io/typescript) : une référence approfondie et gratuite, particulièrement utile pour les mécanismes internes du système de types.

  • TypeScript Playground (typescriptlang.org/play) : l’environnement de test officiel, dans le navigateur, sans installation. Idéal pour expérimenter et partager des exemples.

Exercices et défis

  • type-challenges (github.com/type-challenges/type-challenges) : une collection de défis de typage, du niveau débutant à extrême. C’est la façon la plus efficace de progresser sur les types avancés.

  • Total TypeScript de Matt Pocock : une formation vidéo très appréciée, avec des exercices interactifs dans l’éditeur.

Communauté

  • TypeScript Weekly : une newsletter hebdomadaire sur les nouveautés de l’écosystème.

  • Stack Overflow (typescript tag) et les forums Discord de TypeScript, Vue et React pour obtenir de l’aide sur des problèmes spécifiques.

Résumé général du livre#

Ce livre a couvert TypeScript de A à Z, depuis ses motivations fondamentales jusqu’aux pratiques d’un développeur expérimenté.

Partie I — Fondations : le système de types de TypeScript repose sur la compatibilité structurelle, non nominale. Les types de base, les unions, les intersections et les types littéraux permettent d’exprimer des contraintes précises dès les premiers fichiers.

Partie II — Fonctions et classes : les fonctions bénéficient de surcharges, de paramètres optionnels et de types de retour précis. Les classes combinent l’encapsulation orientée objet avec le système de types, via les modificateurs d’accès et les propriétés en lecture seule.

Partie III — Système de types avancé : les génériques permettent l’abstraction sans sacrifier la sécurité. Les types utilitaires (Partial, Required, Pick, Omit, Record, etc.) transforment les types existants. Les types conditionnels et les types mappés offrent une métaprogrammation de types puissante, que l’on retrouve dans les bibliothèques les plus sophistiquées.

Partie IV — Fonctionnalités du langage : les décorateurs expriment des métadonnées et des comportements transversaux ; le système de modules d’ES2015, combiné à moduleResolution: "bundler", est la base de tout projet moderne.

Partie V — Écosystème : TypeScript s’intègre naturellement dans Node.js, React et Vue 3. Chaque écosystème apporte ses propres patterns idiomatiques — hooks typés, Composition API, stores Pinia — mais tous reposent sur les mêmes fondations du système de types.

Partie VI — Qualité et pratiques : ESLint, Prettier, Vitest et Zod forment le pipeline de qualité standard. Les bonnes pratiques — inférence plutôt qu’annotation excessive, types colocalisés, exhaustivité via never, migration progressive — transforment une base de code fonctionnelle en une base de code maintenable sur le long terme.

TypeScript est un investissement. Les premiers jours peuvent sembler lents, quand le compilateur signale des erreurs que l’on n’aurait pas vues. Mais cet investissement se rentabilise rapidement : les refactorisations deviennent sûres, les erreurs de régression diminuent, les équipes communiquent via des interfaces plutôt que via de la documentation informelle. Le chemin vers la maîtrise passe par la pratique, les défis de typage, et la lecture de bases de code de qualité. Ce livre en est le point de départ ; la suite appartient au lecteur.