Accueil / Articles / Fondamentaux de TypeScript : De la première annotation aux génériques et au mode strict

Fondamentaux de TypeScript : De la première annotation aux génériques et au mode strict

Une présentation structurée du système de types de TypeScript, allant des primitives et de l’inférence aux unions discriminées, aux génériques, aux types utilitaires et à un tsconfig strict.

3750 mots

Tout développeur JavaScript connaît ce schéma : le code fonctionne localement, est déployé, et deux jours plus tard un rapport de bug arrive parce qu’une fonction a reçu un objet au lieu d’une chaîne, ou que undefined s’est glissé dans un calcul. JavaScript ne vous empêche pas d’écrire ce code ; il échoue simplement plus tard, en temps de exécution, généralement en production. TypeScript déplace cet échec au moment même où vous tapez la ligne de code. Ce guide vous accompagne, depuis votre première ligne let x: number, jusqu’aux unions discriminées, aux génériques et aux types utilitaires, en passant par les paramètres du compilateur qui déterminent le niveau de protection réel, afin que vous puissiez lire et écrire du TypeScript en production avec confiance.

Qu’est-ce que TypeScript, et qu’est-ce que ce n’est pas

TypeScript est un langage open source développé par Microsoft qui ajoute un système de types statiques à JavaScript. Ici, « statique » signifie que le compilateur vérifie les types avant l’exécution, dans votre éditeur ou au moment de la compilation, plutôt que de les découvrir pendant l’exécution. Trois faits conditionnent tout le reste :

  • C’est un superensemble de JavaScript. Tout code JavaScript syntaxiquement valide est également une syntaxe TypeScript valide, ce qui vous permet d’étendre vos connaissances existantes plutôt que de tout recommencer. Le vérificateur peut néanmoins signaler des erreurs sur de telles parties de code, et c’est précisément l’objectif.
  • Il se compile en JavaScript pur. Les navigateurs et Node.js exécutent JavaScript, donc le compilateur tsc supprime les annotations et génère des fichiers .js ordinaires.
  • Les types disparaissent en temps de exécution. Ils existent pour vous protéger pendant le développement. Contrairement à Java, où les informations de type survivent dans le bytecode compilé, rien d’un type TypeScript n’existe une fois que le programme est en cours d’exécution.
  • La différence en une fonction

    Voici une fonction JavaScript ordinaire qui additionne deux valeurs :

    function add(a, b) {
      return a + b;
    }
    

    Lorsqu’elle est appelée avec une chaîne de caractères et un nombre, elle ne lance aucune erreur. Elle effectue une concaténation, et vous ne vous en apercevez que lorsque le total d’une facture semble anormal :

    add("10", 20); // "1020" — silently wrong
    

    Indiquer les types des paramètres et de la valeur retournée définit explicitement le contrat :

    function add(a: number, b: number): number {
      return a + b;
    }
    

    Désormais, la même appelation est rejetée tant que vous modifiez encore le code, avec l’erreur affichée directement sous l’argument en cause :

    add("10", 20);
    // Error: Argument of type 'string' is not assignable to parameter of type 'number'.
    

    Mettre en place un projet

    Avec Node.js installé, vous pouvez installer le compilateur de manière globale et vérifier qu’il fonctionne :

    npm install -g typescript
    tsc --version
    

    Pour des projets réels, installez TypeScript en tant que dépendance de développement locale afin que tous les contributeurs et le serveur CI utilisent la même version, puis générez un fichier de configuration :

    npm install typescript --save-dev
    npx tsc --init
    

    Le fichier tsconfig.json généré sert de panneau de contrôle pour définir le niveau de strictesse et la modernité souhaités par le compilateur. Les versions récentes de tsc --init activent déjà le mode strict, et TypeScript 6.0 a fait du mode strict la valeur par défaut, mais les projets plus anciens fonctionnent souvent avec des paramètres moins stricts que les équipes viennent ensuite renforcer. La section de configuration ci-dessous aborde ce sujet.

    // app.ts
    let message: string = "Hello, TypeScript";
    console.log(message);
    

    Compilez-le, puis exécutez le JavaScript généré :

    tsc app.ts     # produces app.js
    node app.js    # Hello, TypeScript
    

    Au quotidien, la plupart des projets évitent ce processus en deux étapes et exécutent les fichiers via ts-node ou tsx, ou bien ils s’appuient sur un bundler tel que Vite, esbuild ou webpack qui compile en temps réel. Il reste utile de le faire manuellement au moins une fois, car cela instaure le bon modèle mental : on entre avec du TypeScript et on obtient du JavaScript.

    Types de base et inférence

    Primitifs et tableaux

    Les annotations pour les primitifs sont string, number et boolean:

    let username: string = "sanajit";
    let age: number = 26;
    let isActive: boolean = true;
    

    Les tableaux s’écrivent avec le type de l’élément suivi de crochets :

    let scores: number[] = [90, 85, 78];
    let tags: string[] = ["typescript", "javascript"];
    

    La forme générique Array<number> signifie exactement la même chose ; choisissez un style et utilisez-le de manière cohérente :

    // equivalent generic syntax
    let ids: Array<number> = [1, 2, 3];
    

    Laissez l’inférence effectuer le travail évident

    Il est rarement nécessaire d’annoter les variables initialisées. TypeScript déduit le type à partir de la valeur puis l’impose :

    let city = "Ahmedabad"; // inferred as string
    city = 42;              // Error: Type 'number' is not assignable to type 'string'
    

    Écrire let city: string = „Ahmedabad“ n’est pas incorrect, mais plutôt redondant. Une bonne règle pour toute la langue : compter sur l’inférence lorsque la valeur rend le type évident, et indiquer explicitement les types lorsque la valeur est ambiguë ou lorsqu’on définit un contrat, comme pour les paramètres de fonction, les types de retour, les tableaux vides et les signatures de fonctions de rappel.

    any, unknown, never et void

    Ces quatre types spéciaux posent problème à presque tous les débutants :

    • any désactive la vérification de type pour une valeur. Il s’agit d’un moyen de contournement plutôt que d’un vrai type, et son utilisation excessive est la cause la plus fréquente pour que un code devienne du JavaScript avec une syntaxe supplémentaire.
  • unknown est l’équivalent sûr. N’importe quelle valeur peut y être assignée, mais vous ne pouvez pas l’utiliser avant de la restreindre par une vérification. C’est le choix idéal pour des données dont la structure n’est pas encore connue, comme une réponse API avant validation.
  • never décrit une valeur qui ne peut pas exister, par exemple le résultat d’une fonction qui lance toujours une erreur ou ne renvoie jamais de valeur. Son utilisation la plus pratique concerne les vérifications d’épuisement, permettant au compilateur de s’assurer que chaque cas d’un switch est traité.
  • void indique une fonction qui ne renvoie rien d’utile, comme un gestionnaire d’événements ou un wrapper de journalisation.
  • Avec unknown, une vérification de typeof ne débloque les méthodes de chaîne que dans la branche protégée :

    function process(value: unknown) {
      if (typeof value === "string") {
        console.log(value.toUpperCase()); // safe — narrowed to string
      }
    }
    

    Une fonction qui lance toujours une exception est typée never, tandis qu’une fonction qui ne fait que produire un effet secondaire renvoie void:

    function fail(message: string): never {
      throw new Error(message);
    }function logAction(action: string): void {
      console.log(`Action: ${action}`);
    }
    

    Pour des remplacements concrets de any dans des situations courantes, consultez six modèles sûrs du point de vue des types pour remplacer any.

    Description d’objets avec des interfaces et des alias de type

    La structure d’un objet peut être écrite en ligne :

    const employee: { id: number; name: string; active: boolean } = {
      id: 1,
      name: "Amit",
      active: true,
    };
    

    Cela fonctionne une fois, mais répéter la même structure en ligne partout devient rapidement source de désordre. Donner un nom à cette structure avec une interface ou un type résout ce problème.

    Interfaces

    interface Employee {
      id: number;
      name: string;
      department: string;
    }
    

    Tout objet annoté avec elle doit correspondre aux champs déclarés :

    const employee1: Employee = { id: 1, name: "John", department: "IT" };
    const employee2: Employee = { id: 2, name: "Sara", department: "HR" };
    

    Un point d’interrogation indique une propriété qui peut être absente :

    interface User {
      id: number;
      name: string;
      phone?: string; // may or may not be present
    }
    

    Les propriétés readonly peuvent être assignées au moment de la création de l’objet, mais pas par la suite :

    interface Product {
      readonly id: number;
      name: string;
    }
    

    Tenter de les réassigner provoque une erreur à la compilation :

    const laptop: Product = { id: 100, name: "MacBook" };
    laptop.id = 200; // Error: Cannot assign to 'id' because it is a read-only property
    

    Les interfaces peuvent également s’étendre les unes aux autres, ce qui permet de partager des champs communs sans les copier. Commencez par une forme de base :

    interface Person {
      name: string;
      age: number;
    }
    

    Puis créez-en une plus spécifique en s’appuyant sur elle :

    interface Employee extends Person {
      employeeId: number;
      department: string;
    }
    

    Aliases de type

    Un alias de type peut également donner un nom aux formes d’objet, mais il n’est pas limité à celles-ci. Les unions, les primitives et les tuples peuvent tous recevoir un nom de cette manière :

    type ID = string | number;
    type Point = { x: number; y: number };
    type Status = "pending" | "shipped" | "delivered";
    

    Choix entre interface et type

    Pour les formes d’objet courantes, les deux sont largement interchangeables. Les véritables différences se situent dans :

    • Déploiement : les interfaces utilisent extends ; les alias de type combinent des formes avec l’opérateur d’intersection &.
    • Union et types primitifs : seuls les alias de type peuvent les exprimer, comme dans type A = string | number.
    • Fusion des déclarations : deux interfaces portant le même nom se fusionnent en une ; les alias de type ne peuvent pas être redéclarés.
    • Convention : les interfaces sont utilisées pour les formes d’API publiques et les contrats de classe ; les alias de type pour les unions, les tuples ainsi que les types conditionnels ou mappés.

    Règle adoptée par de nombreuses équipes : privilégiez interface chaque fois qu’une forme est susceptible d’être étendue, comme les props de React, les modèles d’API et les contrats de classe, et utilisez type pour les unions, les tuples et tous les types non objets.

    Unions, réduction du champ et intersections

    Types d’union

    Une union indique qu’une valeur peut appartenir à l’un de plusieurs types :

    function printId(id: string | number) {
      console.log(`Your ID is ${id}`);
    }
    

    Les deux appels ci-dessous sont acceptés, car chaque argument correspond à un membre de l’union :

    printId(101);
    printId("A-204");
    

    Restreint

    Lorsqu’une valeur est de type union, TypeScript n’autorise que les opérations valables pour tous les membres tant que vous n’avez pas prouvé quel membre vous avez. Cette preuve s’appelle le restreint, et le compilateur l’applique tout au long du flux de contrôle :

    function formatValue(value: string | number) {
      if (typeof value === "string") {
        return value.toUpperCase(); // TypeScript knows it's a string here
      }
      return value.toFixed(2); // and here, it knows it's a number
    }
    

    En plus de typeof, les outils courants de restreint sont Array.isArray(), instanceof, l’opérateur in ainsi que les vérifications d’égalité avec null ou undefined. C’est ainsi que TypeScript reste silencieux dans le cas normal tout en signalant les cas inhabituels.

    Unions discriminées pour l’état

    Le schéma que les développeurs intermédiaires manquent le plus souvent est d’attribuer à chaque variante d’une union un champ littéral « tag » commun. Définissez d’abord chaque état séparément :

    type LoadingState = { status: "loading" };
    type SuccessState = { status: "success"; data: string[] };
    type ErrorState = { status: "error"; message: string };
    

    Ensuite, combinez-les et utilisez le tag pour faire le choix :

    type FetchState = LoadingState | SuccessState | ErrorState;function render(state: FetchState) {
      switch (state.status) {
        case "loading":
          return "Loading...";
        case "success":
          return `Loaded ${state.data.length} items`;
        case "error":
          return `Failed: ${state.message}`;
      }
    }
    

    Dans chaque case, la valeur de state est restreinte à la variante correspondante, de sorte que state.data n’est disponible que dans la branche "success" et state.message uniquement dans la branche "error". Les combinaisons impossibles, comme disposer à la fois de données et d’une erreur, ne peuvent tout simplement pas être représentées. Cela élimine toute une catégorie de plantages liés à des "propriétés non définies" dans le code d’interface utilisateur et d’API. L’ajout d’une branche default qui assigne state à une variable never transforme cela en vérification d’épuisement : si l’on ajoute plus tard un quatrième état, le compilateur indiquera chaque instruction switch qui l’a oublié.

    Types d’intersection

    Lorsqu’une union signifie ">cela ou cela", une intersection signifie "cela et cela". Commencez par deux formes simples :

    type Timestamped = { createdAt: Date };
    type Named = { name: string };
    

    Une intersection exige qu’un objet possède les champs des deux :

    type Record = Timestamped & Named;const item: Record = { name: "Invoice", createdAt: new Date() };
    

    Une précaution concernant cet exemple : Record est également le nom d’un type utilitaire intégré. En déclarant votre propre alias avec ce nom, vous masquez le type global dans ce fichier, ce qui est au mieux déroutant ; préférez donc un nom plus spécifique dans du code réel.

    Fonctions

    Les annotations de paramètre et de retour constituent l’essentiel du contrat d’une fonction :

    function multiply(a: number, b: number): number {
      return a * b;
    }
    

    Les paramètres optionnels utilisent ? ; les valeurs par défaut rendent un paramètre optionnel et en déduisent le type, tandis que les paramètres restants collectent un nombre quelconque d’arguments dans un tableau typé :

    // optional parameter
    function greet(name: string, title?: string): string {
      return title ? `${title} ${name}` : name;
    }// default parameter
    function createOrder(item: string, quantity: number = 1) {
      return { item, quantity };
    }// rest parameters
    function sum(...numbers: number[]): number {
      return numbers.reduce((total, n) => total + n, 0);
    }
    

    Types de fonctions

    Vous pouvez également décrire la structure même d’une fonction, ce qui est utile pour les callbacks et les objets de stratégie :

    type MathOperation = (a: number, b: number) => number;
    

    Une fonction assignée à ce type tire ses types de paramètres de l’annotation, donc a et b n’ont pas besoin d’annotations propres :

    const subtract: MathOperation = (a, b) => a - b;
    

    Classe et modificateurs d’accès

    Les classes TypeScript sont des classes JavaScript dotées de propriétés typées et de modificateurs d’accès. La classe ci-dessous commence par déclarer un solde privé et un propriétaire en lecture seule :

    class Account {
      private balance: number;
      readonly owner: string;
    

    Le reste de la classe définit ces champs dans le constructeur et expose des méthodes pour modifier et lire le solde ; l’accès au champ privé depuis l’extérieur est refusé :

      constructor(owner: string, initialBalance: number) {
        this.owner = owner;
        this.balance = initialBalance;
      }  deposit(amount: number): void {
        this.balance += amount;
      }  getBalance(): number {
        return this.balance;
      }
    }const acc = new Account("Priya", 1000);
    acc.deposit(500);
    console.log(acc.getBalance()); // 1500
    acc.balance; // Error: Property 'balance' is private
    

    Les modificateurs signifient :

    • public, par défaut, est accessible partout.
    • private n’est accessible qu’à l’intérieur de la classe.
    • protected est accessible à l’intérieur de la classe et de ses sous-classes.
  • readonly : ces champs acceptent une valeur lors du constructeur et refusent toute réaffectation ultérieure.
  • N’oubliez pas que le statut private n’est appliqué que par le compilateur ; en temps de exécution, la propriété est un champ ordinaire. Si vous avez besoin d’une véritable confidentialité en temps de exécution, les champs # de JavaScript le permettent, ce qui représente un compromis abordé dans les champs privés de TypeScript versus la syntaxe #.

    Les interfaces en tant que contrats de classe

    interface Shape {
      area(): number;
    }
    

    Avec implements, le compilateur vérifie que la classe respecte ce contrat, et un manque de méthode est signalé avant l’exécution du code. Le constructeur utilise également une propriété paramétrée, private radius, qui déclare et affecte le champ en une seule étape :

    class Circle implements Shape {
      constructor(private radius: number) {}  area(): number {
        return Math.PI * this.radius ** 2;
      }
    }
    

    Generiques : code réutilisable qui conserve ses types

    Les générics sont généralement le point où TypeScript cesse de ressembler à du JavaScript annoté pour devenir un outil distinct.

    Le problème qu’ils résolvent

    Un outil d’aide qui retourne le premier élément de n’importe quel tableau peut être écrit avec any :

    function firstElement(arr: any[]) {
      return arr[0];
    }
    

    Cela fonctionne, mais les informations de type sont perdues lors du retour, et chaque résultat est de type any :

    const num = firstElement([1, 2, 3]);   // typed as `any` — no help from the compiler
    const str = firstElement(["a", "b"]);  // also `any`
    

    Un paramètre de type capture le type de l’élément d’entrée et le réutilise pour la valeur de retour :

    function firstElement<T>(arr: T[]): T {
      return arr[0];
    }
    

    Désormais, chaque appel obtient un type de résultat précis, déduit de l’argument fourni :

    const num = firstElement([1, 2, 3]);   // inferred as number
    const str = firstElement(["a", "b"]);  // inferred as string
    

    T est une variable de type que TypeScript remplit en fonction des valeurs que vous fournissez. Vous conservez la flexibilité de any tout en bénéficiant de la sécurité d’un type concret. Notez que, avec un tableau vide, arr[0] vaut en réalité undefined au moment de l’exécution ; activer noUncheckedIndexedAccess fait en sorte que le compilateur reflète cela sous la forme de T | undefined.

    Interfaces génériques

    Les génériques fonctionnent également avec les interfaces. Un seul conteneur de réponse peut décrire chaque point d’entrée :

    interface ApiResponse<T> {
      success: boolean;
      data: T;
    }
    

    Le type du chargement de données est indiqué là où le conteneur est utilisé :

    const userResponse: ApiResponse<{ id: number; name: string }> = {
      success: true,
      data: { id: 1, name: "Sara" },
    };
    

    C’est ainsi que de nombreuses applications structurent leur couche API : une seule instance de ApiResponse<T> est réutilisée pour les utilisateurs, les produits, les commandes et tout autre élément retourné par le backend.

    Restreindre un paramètre de type

    Parfois, T doit garantir certaines propriétés. Décrivez cette exigence sous forme d’interface :

    interface HasLength {
      length: number;
    }
    

    Ensuite, restreignez le paramètre avec extends, de sorte que seuls les types disposant d’une propriété length soient acceptés :

    function logLength<T extends HasLength>(item: T): void {
      console.log(item.length);
    }logLength("hello");       // OK — strings have .length
    logLength([1, 2, 3]);     // OK — arrays have .length
    logLength(42);             // Error: number doesn't have .length
    

    Types utilitaires

    TypeScript fournit des types d’aide génériques qui dérivent de nouveaux types à partir de ceux existants, remplaçant ainsi une grande partie du code générique manuellement écrit. Étant donné un modèle d’utilisateur :

    interface User {
      id: number;
      name: string;
      email: string;
      isAdmin: boolean;
    }
    

    Vous pouvez dériver des variantes au lieu de les redéclarer : Partial rend chaque propriété optionnelle, Readonly les rend toutes en lecture seule, Pick conserve les clés sélectionnées, Omit les supprime, et Record crée un type de dictionnaire :

    // Every property becomes optional — perfect for "update" functions
    type UserUpdate = Partial<User>;// Every property becomes read-only
    type ImmutableUser = Readonly<User>;// Pick only the fields you need
    type UserPreview = Pick<User, "id" | "name">;// Everything except the fields you list
    type PublicUser = Omit<User, "email" | "isAdmin">;// A dictionary shape: keys of one type, values of another
    type UsersById = Record<number, User>;
    

    Une utilisation typique concerne une fonction de mise à jour qui accepte n’importe quel sous-ensemble de champs :

    function updateUser(id: number, changes: Partial<User>): void {
      // merge `changes` into the stored user
    }
    

    Les appels transmettent uniquement ce qui a changé :

    updateUser(1, { name: "New Name" }); // no need to pass email, isAdmin, etc.
    

    Le guide du blog sur les types utilitaires intégrés de TypeScript explore plus en détail l’ensemble complet de ces types.

    Enums et types littéraux

    Enums

    Un enum définit un ensemble nommé de constantes :

    enum OrderStatus {
      Pending,
      Shipped,
      Delivered,
    }
    

    Les valeurs sont ensuite référencées à travers l’enum :

    let status: OrderStatus = OrderStatus.Shipped;
    

    Par défaut, les membres sont numérotés à partir de 0. En attribuant plutôt des valeurs de chaîne, il devient beaucoup plus facile de déboguer les journaux et les en-têtes réseau :

    enum Direction {
      Up = "UP",
      Down = "DOWN",
      Left = "LEFT",
      Right = "RIGHT",
    }
    

    Les unions de littéraux de chaîne sont souvent plus simples

    De nombreuses équipes préfèrent aujourd’hui une union de littéraux de chaîne, car cela ne génère pas de JavaScript supplémentaire et se comporte de manière plus prévisible :

    type OrderStatus = "pending" | "shipped" | "delivered";
    

    Le compilateur rejette toujours toute valeur en dehors de cet ensemble :

    function updateStatus(status: OrderStatus) {
      // ...
    }updateStatus("shipped");  // OK
    updateStatus("cancelled"); // Error: not assignable to type 'OrderStatus'
    

    Il existe une autre raison pratique de privilégier les unions : les enums génèrent du code à l’exécution, de sorte que les outils qui ne suppriment que les types, tels que le support intégré de TypeScript dans Node.js, ne les acceptent pas.

    Modules et tsconfig.json

    Modules

    TypeScript utilise la syntaxe standard des modules ES. Un fichier exporte une fonction :

    // math.ts
    export function add(a: number, b: number): number {
      return a + b;
    }
    

    Un autre fichier l’importe :

    // app.ts
    import { add } from "./math";
    

    Les paramètres les plus importants

    {
      "compilerOptions": {
        "target": "ES2020",
        "module": "ESNext",
        "strict": true,
        "noImplicitAny": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "outDir": "./dist"
      }
    }
    
    • strict: true active toute la série de vérifications strictes, y compris strictNullChecks, qui oblige à gérer explicitement les valeurs null et undefined. Les développeurs expérimentés le considèrent comme indispensable, car sans lui TypeScript détecte bien moins d’erreurs réelles.
  • noImplicitAny signale une erreur chaque fois qu’une valeur reprendrait par défaut la valeur any sans que vous ne le demandiez. Il fait déjà partie de l’option strict, donc le mentionner séparément ne sert qu’à améliorer la lisibilité.
  • target détermine quelle version de JavaScript sera générée, et donc dans quelle mesure la syntaxe moderne sera réécrite pour des environnements plus anciens.
  • Lorsque vous héritez d’une base de code ancienne avec l’option strict désactivée, la solution durable consiste à l’activer et à corriger les erreurs un fichier à la fois, plutôt que d’utiliser any jusqu’à ce que les erreurs disparaissent.

    Erreurs courantes à chaque niveau

    Débutants

    • Annotationner des valeurs que TypeScript déduirait de toute façon.
    • Recourir à any dès que le compilateur signale un problème, au lieu de préciser le type réel.
    • Considérer les erreurs de compilateur comme anodines alors qu’il s’agit en réalité d’une revue de code gratuite.

    Niveau intermédiaire

    • Déclarer une interface ou un type pour une forme utilisée une seule fois que l’inférence pourrait gérer, ajoutant ainsi de la formalité sans apporter de valeur.
    • Définir des tableaux comme modifiables et les modifier avec push ou splice alors qu’ils devraient être de type ReadonlyArray<T> et mis à jour de manière immuable.
    • Oublier qu’une union doit d’abord être restreinte avant que les membres spécifiques au type ne soient accessibles, et lutter contre le compilateur au lieu de lire son message.

    Niveau avancé

    • Rédiger des génériques non restreints qui pourraient être n’importe quoi, ce qui contredit silencieusement l’objectif même de rendre une fonction générique.
    • Désactiver le mode strict pour tout le projet afin de cacher quelques erreurs, au lieu de les corriger ou de les limiter à un contexte précis.
    • Considérer une assertion de type telle que value as Type comme si elle validait quoi que ce soit. Une assertion ne vérifie rien en temps de exécution ; elle se contente d’indiquer au compilateur de vous faire confiance. Les données provenant à l’extérieur de votre programme, y compris les réponses API, les entrées utilisateur et localStorage, nécessitent une validation réelle.

    Points clés

    • TypeScript ajoute un vérificateur en temps de compilation à JavaScript ; les types disparaissent lorsque le code s’exécute, il reste donc nécessaire de valider les contraintes en temps de exécution.
    • Annotez les contrats tels que les signatures de fonctions et les collections vides, et laissez l’inférence gérer le reste.
    • Utilisez interface pour définir des structures d’objets extensibles et type pour les unions, les tuples et les types non objets.
    • Apprenez tôt les unions discriminées ; c’est le pattern qui empêche le plus directement les erreurs dans les codes à forte composante d’état.
    • Les types génériques et utilitaires tels que Partial, Pick, Omit et Record sont ce qui transforme des annotations dispersées en un véritable système de types.
    • Gardez strict: true activé. La force de TypeScript réside dans sa capacité à détecter les erreurs que l’on découvrirait sinon plus tard et à un coût bien plus élevé, ce qui n’est possible que si le vérificateur a la liberté d’accomplir sa tâche.

    Lectures complémentaires