Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
Angular

Le typage fort en TypeScript

JavaScript convertit silencieusement les valeurs d’un type à l’autre, ce qui produit des bugs difficiles à repérer, comme '45' + 7 qui donne '457'. Ce chapitre explique la différence entre typage stat...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

JavaScript convertit silencieusement les valeurs d’un type à l’autre, ce qui produit des bugs difficiles à repérer, comme '45' + 7 qui donne '457'. Ce chapitre explique la différence entre typage statique et dynamique, fort et faible, et montre comment TypeScript refuse ces mélanges douteux. Vous apprendrez à convertir explicitement vos données, à comprendre le typage structurel et à exploiter toutes les options du mode strict. Vous écrirez aussi des gardes de type pour valider des données externes et des unions discriminées qui garantissent qu’aucun cas n’est oublié. Le chapitre montre enfin pourquoi éviter any, as, ! et @ts-ignore, et comment le typage fort s’applique dans Angular, avant cinq activités pratiques.
  1. Objectifs

    • Distinguer typage statique / dynamique et typage fort / faible
    • Repérer les conversions implicites de JavaScript et les remplacer par des conversions explicites
    • Comprendre le typage structurel: quand deux types sont-ils compatibles ?
    • Connaître les options du mode strict et les options de rigueur supplémentaires
    • Écrire une garde de type personnalisée (valeur is Type)
    • Utiliser les unions discriminées et vérifier qu'un switch traite tous les cas
    • Reconnaître et éviter les échappatoires: any, as, !, @ts-ignore
  2. Présentation

    • On classe les langages selon deux questions indépendantes:
      • Quand les types sont-ils vérifiés ? Avant l'exécution (typage statique) ou pendant (typage dynamique) ;
      • Avec quelle rigueur ? Le langage convertit-il silencieusement une valeur d'un type vers un autre (typage faible), ou refuse-t-il de mélanger les types (typage fort) ?
    • JavaScript est à la fois dynamique et faible: il ne vérifie rien à l'avance et convertit tout seul, ce qui produit des résultats surprenants.
    • TypeScript ajoute une vérification statique et des règles fortes: il refuse les mélanges douteux et oblige à écrire les conversions.

Typage statique ou dynamique, fort ou faible

  • Remarque : « fort » et « faible » ne sont pas des catégories absolues mais une échelle. TypeScript est nettement plus fort que JavaScript, mais il accepte encore certaines conversions (par exemple '5' + 2, qui est une concaténation légale). Ce chapitre vous apprend à pousser la rigueur au maximum.
  • Le typage faible de JavaScript

    • Quand un opérateur reçoit des valeurs de types différents, JavaScript applique une conversion implicite (ou coercition) : il transforme l'une des valeurs pour que l'opération réussisse, au lieu de signaler une erreur.
    console.log('5' * 2);           // 10   : '5' converti en nombre
    console.log('5' + 2);           // '52' : 2 converti en chaîne !
    console.log('5' - 2);           // 3
    console.log(true + 1);          // 2    : true vaut 1
    console.log([] + {});           // '[object Object]'
    console.log(null + 1);          // 1    : null vaut 0
    console.log(undefined + 1);     // NaN
    console.log('10' == 10);        // true : == convertit avant de comparer
    console.log(0 == '');           // true
    console.log(Boolean('0'), Boolean(''), Boolean([]));   // true false true
    console.log('3' > '12', 3 > '12');                     // true false
    • Le danger : une valeur lue dans un formulaire est toujours une chaîne. Selon l'opérateur utilisé, le calcul est juste (*, -) ou faux (+), sans aucune alerte.
  • Ce que TypeScript refuse

    • Les mêmes expressions en TypeScript sont presque toutes refusées à la compilation :

    Les conversions implicites de JavaScript refusées par TypeScript

    console.log('5' * 2);
    // error TS2362: The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
    console.log(true + 1);
    // error TS2365: Operator '+' cannot be applied to types 'boolean' and 'number'.
    console.log(null + 1);
    // error TS18050: The value 'null' cannot be used here.
    
    const quantite: number = 10;
    const saisie: string = '10';
    console.log(saisie == quantite);
    // error TS2367: This comparison appears to be unintentional because the types 'string' and 'number' have no overlap.
    console.log(3 > '12');
    // error TS2365: Operator '>' cannot be applied to types 'number' and 'string'.
    
    console.log('5' + 2);      // accepté : la concaténation est une opération légale
    • Attention : chaîne + nombre reste autorisé, car c'est une manière courante de construire du texte. C'est le piège le plus fréquent qui échappe à TypeScript : convertissez toujours vos saisies avant de calculer.
  • Convertir explicitement

    • Avec un typage fort, c'est au développeur d'écrire la conversion. On choisit la fonction selon le besoin :
    Conversion Fonction Exemple
    chaîne → nombre (strict) Number(x) Number('12.5') → 12.5; Number('12 ans') → NaN
    chaîne → entier (début de chaîne) Number.parseInt(x, 10) parseInt('42 ans', 10) → 42
    chaîne → décimal (début de chaîne) Number.parseFloat(x) parseFloat('15,5') → 15 (la virgule arrête la lecture)
    nombre → chaîne String(n), n.toFixed(d) (45).toFixed(3) → '45.000'
    valeur → booléen Boolean(x), ou mieux une comparaison saisie.length > 0 plutôt que Boolean(saisie)
    const saisie: string = '12.5';           // valeur lue dans un champ de formulaire
    
    const note: number = Number(saisie);     // 12.5
    const entier: number = parseInt('42 ans', 10);   // 42 : lit le début de la chaîne
    const decimal: number = parseFloat('15,5');      // 15 : la virgule arrête la lecture
    const invalide: number = Number('abc');          // NaN
    
    console.log(note + 2, entier, decimal, invalide);   // 14.5 42 15 NaN
    console.log(Number.isNaN(invalide));                 // true : toujours tester NaN
    
    const texte: string = String(2026);       // '2026'
    const prix: string = (45).toFixed(3);     // '45.000'
    const actif: boolean = Boolean('non');    // true : toute chaîne non vide est vraie
    const vide: boolean = saisie.length > 0;  // préférer une comparaison explicite
    • Remarque : NaN est de type number. TypeScript ne peut donc pas savoir si une conversion a échoué : testez toujours le résultat avec Number.isNaN(). Et comparez toujours avec ===, jamais avec ==.
  • Le typage structurel

    • Pour décider si une valeur convient à un type, TypeScript compare sa forme (ses propriétés et leurs types), et non son nom. C'est le typage structurel, aussi appelé « typage canard » : si ça marche comme un canard et cancane comme un canard, c'est un canard.
    • Une valeur est compatible si elle possède au moins toutes les propriétés demandées, avec les bons types.

    Le typage structurel en TypeScript

    interface Personne {
      nom: string;
      email: string;
    }
    
    const envoyerMail = (p: Personne): string => `Mail envoyé à ${p.nom} <${p.email}>`;
    
    const ali = { nom: 'Ali', email: 'ali@formaplus.tn', specialite: 'Angular' };
    const amira = { nom: 'Amira', email: 'amira@exemple.tn', groupe: 'DEV1' };
    
    console.log(envoyerMail(ali));     // OK : ali possède nom et email (string)
    console.log(envoyerMail(amira));   // OK : les propriétés en trop sont tolérées
    
    envoyerMail({ nom: 'Sami', email: 'sami@exemple.tn', groupe: 'DEV2' });
    // error TS2353: Object literal may only specify known properties, and 'groupe' does not exist in type 'Personne'.
    
    envoyerMail({ nom: 'Karim' });
    // error TS2741: Property 'email' is missing in type '{ nom: string; }' but required in type 'Personne'.
    
    const karim = { nom: 'Karim', email: 2026 };
    envoyerMail(karim);
    // error TS2345: Argument of type '{ nom: string; email: number; }' is not assignable to parameter of type 'Personne'.
    //   Types of property 'email' are incompatible.
    //     Type 'number' is not assignable to type 'string'.
    • Pourquoi amira est accepté et l'objet de Sami refusé, alors qu'ils ont tous deux une propriété groupe en trop ?
      • un objet stocké dans une variable peut servir ailleurs : ses propriétés supplémentaires sont tolérées ;
      • un objet écrit directement dans l'appel (ou dans une affectation typée) ne sert qu'ici : une propriété inconnue y est sûrement une faute de frappe. TypeScript applique alors une vérification plus stricte, la vérification des propriétés en excès.
  • Le mode strict en détail

    • "strict": true n'est pas une option unique : c'est un raccourci qui active toute une famille de vérifications. Depuis TypeScript 6, il est activé par défaut.

    Les options de rigueur du compilateur TypeScript

    // noImplicitAny : un paramètre doit être typé
    function doubler(x) { return x * 2; }
    // error TS7006: Parameter 'x' implicitly has an 'any' type.
    
    // strictNullChecks : find() peut ne rien trouver
    const titres: string[] = ['Angular', 'Flutter'];
    const trouve = titres.find((t) => t.startsWith('R'));
    console.log(trouve.length);
    // error TS18048: 'trouve' is possibly 'undefined'.
    
    // strictPropertyInitialization : une propriété de classe doit être initialisée
    class Panier {
      proprietaire: string;
      articles: string[] = [];
    }
    // error TS2564: Property 'proprietaire' has no initializer and is not definitely assigned in the constructor.
    
    // useUnknownInCatchVariables : on ne sait pas ce qui a été lancé
    try {
      JSON.parse('{ invalide');
    } catch (erreur) {
      console.log(erreur.message);
      // error TS18046: 'erreur' is of type 'unknown'.
    }
    • Pour l'erreur attrapée par catch, on vérifie son type avant de l'utiliser : if (erreur instanceof Error) console.log(erreur.message);
    • Options supplémentaires: elles ne font pas partie de strict et s'ajoutent une à une dans tsconfig.json. Angular en active plusieurs dans tout nouveau projet.
    {
      "compilerOptions": {
        "strict": true,
        "noUncheckedIndexedAccess": true,
        "noImplicitReturns": true,
        "noFallthroughCasesInSwitch": true,
        "exactOptionalPropertyTypes": true
      }
    }
    // noUncheckedIndexedAccess : une case de tableau peut ne pas exister
    const notes: number[] = [12, 15];
    const troisieme = notes[2];          // number | undefined
    console.log(troisieme.toFixed(1));
    // error TS18048: 'troisieme' is possibly 'undefined'.
    
    const coefficients: Record<string, number> = { Angular: 3 };
    console.log(coefficients['Flutter'] * 2);
    // error TS2532: Object is possibly 'undefined'.
    
    // noImplicitReturns : toutes les branches doivent renvoyer une valeur
    function mention(moyenne: number) {
      if (moyenne >= 16) return 'Très bien';
      else if (moyenne >= 10) return 'Admis';
    }
    // error TS7030: Not all code paths return a value.
    
    // noFallthroughCasesInSwitch : pas de case qui « tombe » dans le suivant
    function couleur(statut: 'ok' | 'erreur'): string {
      let c = '';
      switch (statut) {
        case 'ok':
          c = 'success';       // break oublié !
        case 'erreur':
          c = 'danger';
      }
      return c;
    }
    // error TS7029: Fallthrough case in switch.
    
    // exactOptionalPropertyTypes : « optionnel » veut dire « absent », pas « undefined »
    interface Options { theme?: 'clair' | 'sombre' }
    const options: Options = { theme: undefined };
    // error TS2375: Type '{ theme: undefined; }' is not assignable to type 'Options' with 'exactOptionalPropertyTypes: true'. …
    • Remarque : noUncheckedIndexedAccess est l'option qui attrape le plus de bugs réels (index hors limites, clé absente d'un dictionnaire), mais elle oblige à écrire ?. ou ?? à chaque accès. Elle est recommandée par tsc --init.
  • Les gardes de type personnalisées

    • Les tests typeof, in ou Array.isArray rétrécissent un type, mais seulement à l'endroit où ils sont écrits. Pour réutiliser une vérification, on l'enferme dans une fonction dont le type de retour est valeur is Type: c'est une garde de type (type guard).
    • Quand la garde renvoie true, TypeScript considère la valeur comme étant du type annoncé. C'est l'outil idéal pour valider les données d'une API.
    interface Stagiaire {
      nom: string;
      moyenne: number;
    }
    
    // Garde de type : renvoie un boolean, et informe TypeScript du type
    function estStagiaire(valeur: unknown): valeur is Stagiaire {
      return (
        typeof valeur === 'object' &&
        valeur !== null &&
        'nom' in valeur &&
        typeof valeur.nom === 'string' &&
        'moyenne' in valeur &&
        typeof valeur.moyenne === 'number'
      );
    }
    
    const donnees: unknown = JSON.parse('[{"nom":"Amira","moyenne":15.5},{"nom":"Sami"},42]');
    
    if (Array.isArray(donnees)) {
      const stagiaires = donnees.filter(estStagiaire);     // Stagiaire[]
      for (const s of stagiaires) {
        console.log(`${s.nom} : ${s.moyenne.toFixed(2)}`); // Amira : 15.50
      }
      console.log(`${donnees.length - stagiaires.length} élément(s) ignoré(s)`);   // 2 élément(s) ignoré(s)
    }
    • Attention : TypeScript fait confiance au corps de la garde. Si elle renvoie true sans tout vérifier, le typage devient faux. Une garde doit tester chaque propriété utilisée ensuite.
  • Les unions discriminées et l'exhaustivité

    • Une union discriminée est une union d'objets qui partagent une propriété dont la valeur est un type littéral différent pour chaque forme (ici mode). Tester cette propriété suffit à TypeScript pour savoir quelle forme est présente.
    • Dans le default, toutes les formes ont été traitées : la valeur est de type never. L'affecter à une variable never est une vérification d'exhaustivité: si l'on ajoute une forme sans la traiter, la compilation échoue.

    Union discriminée et vérification d'exhaustivité

    type Paiement =
      | { mode: 'especes'; montant: number }
      | { mode: 'carte'; montant: number; numero: string }
      | { mode: 'virement'; montant: number; iban: string };
    
    function recu(p: Paiement): string {
      switch (p.mode) {
        case 'especes':
          return `${p.montant.toFixed(3)} DT en espèces`;
        case 'carte':
          return `${p.montant.toFixed(3)} DT par carte **** ${p.numero.slice(-4)}`;   // p.numero existe ici
        case 'virement':
          return `${p.montant.toFixed(3)} DT par virement (${p.iban})`;
        default: {
          const impossible: never = p;     // vérification d'exhaustivité
          return impossible;
        }
      }
    }
    
    console.log(recu({ mode: 'especes', montant: 250 }));
    // 250.000 DT en espèces
    console.log(recu({ mode: 'carte', montant: 480, numero: '4970101234567890' }));
    // 480.000 DT par carte **** 7890
    console.log(recu({ mode: 'virement', montant: 320, iban: 'TN59 1000 6035' }));
    // 320.000 DT par virement (TN59 1000 6035)
    • Si l'on ajoute | { mode: 'cheque'; montant: number; numero: string } au type sans modifier recu:
    error TS2322: Type '{ mode: "cheque"; montant: number; numero: string; }' is not assignable to type 'never'.
    • Sans le switch, accéder à p.numero est refusé, car cette propriété n'existe pas dans toutes les formes de l'union (error TS2339).
  • Les échappatoires à éviter

    • TypeScript offre des moyens de désactiver ses vérifications. Chacun est une promesse que le compilateur ne contrôle pas : si elle est fausse, l'erreur revient à l'exécution.

    Les échappatoires du typage, du plus sûr au plus dangereux

    const titres: string[] = ['Angular', 'Flutter'];
    
    // 1. any : la contagion
    const brut: any = JSON.parse('{"prix": "45"}');
    const prix: number = brut.prix;            // accepté… mais prix contient la chaîne '45'
    console.log(prix + 10);                    // 4510 : concaténation, pas addition !
    
    // 2. as : une affirmation, pas une conversion
    const valeur = '45' as unknown as number;  // double assertion : tsc ne dit plus rien
    console.log(valeur.toFixed(2));            // TypeError à l'exécution
    
    // 3. ! : « ce n'est pas undefined, crois-moi »
    const premierR = titres.find((t) => t.startsWith('R'))!;
    console.log(premierR.length);              // TypeError à l'exécution
    
    // 4. @ts-ignore : fait taire la ligne suivante, quelle que soit l'erreur
    // @ts-ignore
    const total: number = 'cent';
    
    // 5. @ts-expect-error : comme @ts-ignore, mais signale quand l'erreur a disparu
    // @ts-expect-error
    const quantite: number = 3;
    // error TS2578: Unused '@ts-expect-error' directive.
    • L'exemple de any est le plus traître : prix est annoté number, mais contient une chaîne. Le typage fort est rompu sans aucun message, et le bug apparaît plus loin.
    • Les alternatives sûres :
      • à la place de any: unknown + une garde de type ;
      • à la place de as: une vraie conversion (Number()) ou une garde ; pour vérifier un objet, satisfies;
      • à la place de !: un test, ?. ou ??;
      • à la place de @ts-ignore: corriger le code, ou en dernier recours @ts-expect-error accompagné d'un commentaire.
  • Le typage fort dans Angular

    • Un projet créé avec ng new est strict par défaut. Voici le tsconfig.json généré par Angular 22 (extrait) : strict n'y figure plus car il est actif par défaut, et quatre options de rigueur sont ajoutées.
    {
      "compilerOptions": {
        "noImplicitOverride": true,
        "noPropertyAccessFromIndexSignature": true,
        "noImplicitReturns": true,
        "noFallthroughCasesInSwitch": true,
        "skipLibCheck": true,
        "isolatedModules": true,
        "experimentalDecorators": true,
        "importHelpers": true,
        "target": "ES2022",
        "module": "preserve"
      },
      "angularCompilerOptions": {
        "enableI18nLegacyMessageIdFormat": false,
        "strictInjectionParameters": true,
        "strictInputAccessModifiers": true
      }
    }
    • Les templates sont eux aussi vérifiés en mode strict : une propriété mal orthographiée dans {{ ... }} ou un mauvais type passé à un composant bloque la compilation.
    • Les formulaires réactifs sont typés : new FormControl('') est de type FormControl<string | null> (la valeur redevient null après un reset()), tandis que { nonNullable: true } donne un FormControl<string>.
    import { Component, inject } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
    
    interface Inscription {
      nom: string;
      email: string;
      formation: 'angular' | 'flutter';
    }
    
    @Component({
      selector: 'app-inscription',
      imports: [ReactiveFormsModule],
      template: `
        <form [formGroup]="formulaire" (ngSubmit)="envoyer()">
          <input formControlName="nom" />
          <input formControlName="email" type="email" />
          <select formControlName="formation">
            <option value="angular">Angular</option>
            <option value="flutter">Flutter</option>
          </select>
          <button [disabled]="formulaire.invalid">S'inscrire</button>
        </form>
      `,
    })
    export class InscriptionForm {
      private http = inject(HttpClient);
    
      formulaire = new FormGroup({
        nom: new FormControl('', { nonNullable: true, validators: Validators.required }),
        email: new FormControl('', { nonNullable: true, validators: Validators.email }),
        formation: new FormControl<Inscription['formation']>('angular', { nonNullable: true }),
      });
    
      envoyer(): void {
        const donnees: Inscription = this.formulaire.getRawValue();   // vérifié par TypeScript
        this.http.post<{ id: number }>('/api/inscriptions', donnees)
          .subscribe((reponse) => console.log(`Inscription n° ${reponse.id}`));
      }
    }
    • Avec les contrôles non typés ou nullable, l'affectation const donnees: Inscription = ... serait refusée (Type 'string | null' is not assignable to type 'string') : le typage fort vous oblige à traiter le cas d'un champ vide.
    • Attention : http.post<{ id: number }> ou http.get<Formation[]> ne vérifient pas la réponse du serveur : c'est une affirmation, comme as. Pour des données dont vous n'êtes pas sûr, validez-les avec une garde de type.
  • Activité 1 : du typage faible au typage fort

    • Le programme JavaScript suivant calcule le total d'une commande à partir de valeurs saisies dans un formulaire. L'exécuter avec Node et noter le résultat.
    const prixSaisi = '45.500';
    const quantiteSaisie = '2';
    const remiseSaisie = '';
    const fraisLivraison = 7;
    
    const sousTotal = prixSaisi * quantiteSaisie;
    const remise = sousTotal * remiseSaisie / 100;
    const total = sousTotal - remise + fraisLivraison;
    const affichage = prixSaisi + fraisLivraison;
    
    console.log(sousTotal, remise, total, affichage);
    console.log(quantiteSaisie == 2, remiseSaisie == 0);
    
    // Node affiche :
    // 91 0 98 45.5007
    // true true
    • Copier ce code dans src/11-panier.ts et compiler : TypeScript signale 5 erreurs. Noter leur code et leur ligne.
    • Une ligne contient pourtant un bug que TypeScript n'a pas signalé : laquelle, et pourquoi ?
    • Corriger le programme avec des conversions explicites (Number, Number.parseInt), des variables annotées et ===, pour obtenir :
    91 0 98 52.5
    true true
    1. Activité 2 : compatible ou pas ?

      • Dans src/12-etiquettes.ts, recopier le code suivant. Avant de compiler, indiquer en commentaire pour chaque ligne marquée A à F : « accepté » ou « refusé », avec la raison.
    interface Produit {
      nom: string;
      prix: number;
    }
    
    const etiquette = (p: Produit): string => `${p.nom.padEnd(10, '.')} ${p.prix.toFixed(3)} DT`;
    
    const clavier = { nom: 'Clavier', prix: 45, stock: 12 };
    const souris = { nom: 'Souris', prix: '20' };
    const ecran = { nom: 'Écran', prix: 420, garantie: '2 ans' } satisfies Produit;
    
    console.log(etiquette(clavier));                              // A
    console.log(etiquette(souris));                               // B
    console.log(etiquette({ nom: 'Tapis', prix: 8.5 }));          // C
    console.log(etiquette({ nom: 'Casque', prix: 89, couleur: 'noir' }));  // D
    console.log(etiquette(ecran));                                // E
    console.log(etiquette({ nom: 'Câble' }));                     // F
    • Compiler et comparer avec vos prévisions. Attention au cas E : à quelle ligne l'erreur est-elle signalée ?
    • Corriger les lignes refusées pour que tout le fichier compile, sans utiliser as ni any.
  • Activité 3 : plus strict que strict

    • Créer src/13-planning.ts avec le code ci-dessous et compiler : avec la configuration du cours, aucune erreur.
    type Jour = 'lundi' | 'mardi' | 'mercredi' | 'jeudi' | 'vendredi';
    
    const salles: string[] = ['B12', 'B14', 'Labo 1'];
    const formateurs: Record<string, string> = { Angular: 'Ali', Flutter: 'Mouna' };
    
    function salleDuJour(numero: number): string {
      return salles[numero].toUpperCase();
    }
    
    function formateurDe(module: string): string {
      return formateurs[module].toUpperCase();
    }
    
    function dureeSeance(jour: Jour) {
      switch (jour) {
        case 'lundi':
        case 'mardi':
          return 3;
        case 'mercredi':
          console.log('Demi-journée');
        case 'jeudi':
          return 4;
        case 'vendredi':
          if (Math.random() > 2) return 2;
      }
    }
    
    console.log(salleDuJour(0), salleDuJour(3));
    console.log(formateurDe('Angular'), formateurDe('TypeScript'));
    console.log(dureeSeance('mercredi'), dureeSeance('vendredi'));
    • Exécuter node src/13-planning.ts: que se passe-t-il ?
    • Ajouter dans tsconfig.json les options noUncheckedIndexedAccess, noImplicitReturns et noFallthroughCasesInSwitch, puis recompiler : 4 erreurs apparaissent. Les relier aux bugs du programme.
    • Corriger le code (sans désactiver les options) : une salle inexistante affiche NON ATTRIBUÉE, un module sans formateur affiche À DÉFINIR, le mercredi et le vendredi durent 2 heures, et dureeSeance déclare son type de retour.
    B12 NON ATTRIBUÉE
    ALI À DÉFINIR
    2 2
    • Pourquoi case 'lundi': suivi directement de case 'mardi': n'est-il pas signalé par noFallthroughCasesInSwitch?
  • Activité 4 : valider les données d'une API

    • Dans src/14-catalogue-api.ts, déclarer le type Niveau ('debutant' | 'intermediaire' | 'avance') et l'interface Formation (titre: string, niveau: Niveau, places: number).
    • Écrire deux gardes de type : estNiveau(valeur: unknown): valeur is Niveau et estFormation(valeur: unknown): valeur is Formation (le nombre de places doit être positif ou nul).
    • Analyser la réponse ci-dessous dans une variable unknown, garder uniquement les formations valides avec filter, et afficher :
    const reponseApi = `[
      { "titre": "Angular", "niveau": "intermediaire", "places": 12 },
      { "titre": "Flutter", "niveau": "expert", "places": 8 },
      { "titre": "TypeScript", "niveau": "debutant", "places": "dix" },
      { "titre": "Node.js", "niveau": "avance", "places": 5 },
      null
    ]`;
    
    // Résultat attendu :
    // Angular (intermediaire) : 12 place(s)
    // Node.js (avance) : 5 place(s)
    // 2 formation(s) valide(s), 3 rejetée(s)
    • Remplacer temporairement la validation par JSON.parse(reponseApi) as Formation[]: combien de formations sont affichées, et que provoque l'élément null?
  • Activité 5 : une union discriminée exhaustive

    • Dans src/15-alertes.ts, déclarer le type Alerte, union discriminée par la propriété canal:
      • 'email' avec destinataire et sujet;
      • 'sms' avec telephone et texte;
      • 'push' avec appareil, titre et urgent (booléen).
    • Écrire verifierExhaustivite(valeur: never): never qui lance une erreur, puis decrire(a: Alerte): string avec un switch qui appelle verifierExhaustivite dans le default.
    const envois: Alerte[] = [
      { canal: 'email', destinataire: 'amira@exemple.tn', sujet: 'Convocation' },
      { canal: 'sms', telephone: '22 123 456', texte: 'Cours annulé demain' },
      { canal: 'push', appareil: 'Android', titre: 'Nouvelle note', urgent: true },
    ];
    envois.forEach((a) => console.log(decrire(a)));
    
    // Résultat attendu :
    // Email à amira@exemple.tn – « Convocation »
    // SMS au 22 123 456 (19 caractères)
    // Push sur Android [URGENT] – Nouvelle note
    • Ajouter au type le canal 'whatsapp' (avec telephone et texte) sans modifier decrire. Noter l'erreur, puis traiter ce nouveau cas.
    • Faire un commit après chaque activité, puis un push : git commit -m "Typage fort : activité N".
  • Erreurs fréquentes

  • Message ou symptôme Cause Solution
    error TS2362 / TS2363: The left-hand (right-hand) side of an arithmetic operation must be of type … 'number' … Calcul avec une chaîne (souvent une saisie de formulaire) Convertir d'abord : Number(saisie)
    error TS2367: This comparison appears to be unintentional because the types 'string' and 'number' have no overlap. Comparaison de deux types différents Convertir l'une des valeurs, et comparer avec ===
    Résultat du type '4510' au lieu de 55, sans erreur chaîne + nombre: concaténation acceptée par TypeScript Convertir les saisies avant tout calcul ; bannir any
    NaN affiché alors que le code compile Conversion d'une chaîne invalide (NaN est un number) Tester avec Number.isNaN() après chaque conversion
    error TS2353: Object literal may only specify known properties… Propriété en trop (souvent une faute de frappe) dans un objet écrit directement Corriger le nom, ou ajouter la propriété au type
    error TS2741: Property 'x' is missing in type … Propriété obligatoire absente Fournir la propriété, ou la rendre optionnelle dans le type
    error TS2564: Property 'x' has no initializer… Propriété de classe jamais initialisée L'initialiser à la déclaration ou dans le constructeur
    error TS18046: 'erreur' is of type 'unknown'. dans un catch On ne sait pas ce qui a été lancé if (erreur instanceof Error) { … erreur.message … }
    error TS2532: Object is possibly 'undefined'. sur t[i] noUncheckedIndexedAccess: la case peut ne pas exister t[i]?.… ?? valeurParDefaut, ou tester la valeur
    error TS7030: Not all code paths return a value. Une branche ne renvoie rien Ajouter le return manquant et annoter le type de retour
    error TS7029: Fallthrough case in switch. break ou return oublié dans un case Terminer chaque case non vide par break ou return
    Type '…' is not assignable to type 'never'. Un cas d'une union discriminée n'est pas traité Ajouter le case manquant
    error TS2578: Unused '@ts-expect-error' directive. L'erreur attendue n'existe plus Supprimer la directive
    TypeError à l'exécution alors que tout compile any, as, ! ou @ts-ignore ont désactivé la vérification Remplacer par unknown, une garde de type, ?. / ??
    1. À retenir

      • Statique / dynamique: quand le type est vérifié ; fort / faible: si le langage convertit tout seul. JavaScript est dynamique et faible ; TypeScript ajoute une vérification statique et des règles fortes.
      • TypeScript refuse les calculs et comparaisons entre types différents, mais accepte chaîne + nombre: convertissez explicitement avec Number(), String(), et testez NaN.
      • Le typage structurel compare les formes : un objet compatible a au moins les propriétés demandées. Les objets écrits directement ne doivent pas avoir de propriétés inconnues.
      • "strict": true (par défaut) regroupe noImplicitAny, strictNullChecks… ; noUncheckedIndexedAccess, noImplicitReturns, noFallthroughCasesInSwitch renforcent encore la rigueur.
      • Une garde de type valeur is T valide une donnée unknown; une union discriminée + never garantit qu'aucun cas n'est oublié.
      • any, as, ! et @ts-ignore rompent le typage fort : à remplacer par des vérifications réelles.
    Le typage fort ne se décrète pas, il se protège : chaque conversion écrite, chaque donnée vérifiée et chaque any évité garde à TypeScript le pouvoir de trouver vos bugs à votre place.