Maîtriser TypeScript : Développez des Applications Web Robustes et Scalables
Maîtriser TypeScript : Développez des Applications Web Robustes et Scalables

Maîtriser TypeScript : Développez des Applications Web Robustes et Scalables

Les Types de Base et l'Inférence de Type

Introduction

Dans le monde du développement web moderne, la robustesse et la maintenabilité des applications sont primordiales. JavaScript, avec sa nature dynamique, peut parfois rendre la détection d'erreurs difficile avant l'exécution. C'est là que TypeScript entre en jeu. En tant que superset typé de JavaScript, TypeScript nous offre la possibilité de définir et de vérifier les types de nos données, garantissant ainsi une meilleure intégrité du code et une détection précoce des erreurs.

Cette leçon explorera les fondations de la typification en TypeScript : les types de base. Nous verrons comment déclarer des variables avec ces types, et comment TypeScript peut déduire automatiquement le type d'une variable ou d'une expression, un mécanisme appelé inférence de type. Comprendre ces concepts est essentiel pour écrire du code TypeScript propre, sûr et performant.

Les Types de Base en TypeScript

TypeScript étend JavaScript en ajoutant un système de types statique. Cela signifie que vous pouvez spécifier le type des variables, des paramètres de fonction et des valeurs de retour. Voici les types de base les plus couramment utilisés en TypeScript :

number

Le type number représente les nombres à virgule flottante (y compris les entiers) comme en JavaScript. Cela inclut les entiers, les décimaux, les nombres hexadécimaux (0x...), binaires (0b...), et octaux (0o...).

let age: number = 30;
let prix: number = 99.99;
let hexValue: number = 0xAF; // 175 en décimal

string

Le type string représente les séquences de caractères textuels. Vous pouvez utiliser des guillemets simples ('), doubles ("), ou des backticks (`) pour les template literals.

let nom: string = "Alice";
let message: string = 'Bonjour le monde !';
let prenom: string = `Bob`;
let salutation: string = `Bonjour, ${nom} !`; // Template literal

boolean

Le type boolean représente une valeur logique qui peut être true (vrai) ou false (faux).

let estActif: boolean = true;
let estConnecte: boolean = false;

array

Le type array représente une collection ordonnée d'éléments du même type. Vous pouvez le déclarer de deux manières :

  1. Type[] (syntaxe préférée) : string[], number[], etc.
  2. Array<Type> (syntaxe générique) : Array<string>, Array<number>, etc.
let nombres: number[] = [1, 2, 3, 4, 5];
let nomsUtilisateurs: Array<string> = ["Alice", "Bob", "Charlie"];

tuple

Un tuple est un tableau dont le nombre d'éléments est fixe et dont les types de chaque élément à une position donnée sont connus. Les tuples sont utiles lorsque vous avez un tableau avec un nombre fixe d'éléments de types différents mais prédéfinis.

// Un tuple représentant un point (x, y)
let point: [number, number] = [10, 20];

// Un tuple pour un utilisateur (id, nom, estAdmin)
let utilisateur: [number, string, boolean] = [1, "David", true];

// Accès aux éléments
console.log(utilisateur[1]); // "David"

enum

Un enum (énumération) est un moyen de définir un ensemble de constantes nommées. Par défaut, les valeurs des énumérations commencent à 0 et sont incrémentées pour chaque membre. Vous pouvez également attribuer des valeurs numériques ou des chaînes de caractères.

// Enumération numérique
enum Direction {
    Nord,    // 0
    Est,     // 1
    Sud,     // 2
    Ouest    // 3
}
let dir: Direction = Direction.Nord;
console.log(dir); // 0

// Enumération avec valeurs numériques personnalisées
enum StatusCode {
    NotFound = 404,
    Success = 200,
    Forbidden = 403
}
let status: StatusCode = StatusCode.Success;
console.log(status); // 200

// Enumération de chaînes de caractères
enum JoursDeLaSemaine {
    Lundi = "LUNDI",
    Mardi = "MARDI",
    Mercredi = "MERCREDI"
}
let aujourdhui: JoursDeLaSemaine = JoursDeLaSemaine.Lundi;
console.log(aujourdhui); // "LUNDI"

any

Le type any est le type le plus permissif. Il vous permet d'affecter n'importe quelle valeur à une variable et d'appeler n'importe quelle méthode sur celle-ci, sans vérification de type. C'est l'équivalent de l'absence de type en JavaScript. Bien qu'utile pour la migration de code JavaScript existant ou pour interagir avec des bibliothèques externes non typées, il est fortement déconseillé de l'utiliser à l'excès, car il annule les avantages de la vérification de type de TypeScript.

let inconnu: any = 4;
inconnu = "Peut être une chaîne";
inconnu = false;
inconnu.maMethode(); // Aucune erreur au moment de la compilation, mais potentiellement à l'exécution

void

Le type void est utilisé pour les fonctions qui ne retournent aucune valeur. C'est l'opposé de any : any accepte n'importe quoi, void n'accepte rien.

function afficherMessage(message: string): void {
    console.log(message);
}
afficherMessage("Hello TypeScript!");

null et undefined

En TypeScript (comme en JavaScript), null et undefined sont des types primitifs et ont leurs propres types éponymes : null et undefined. Par défaut, null et undefined sont des sous-types de tous les autres types. Cela signifie que vous pouvez assigner null ou undefined à une variable de type number, string, etc. Cependant, avec le flag de compilation strictNullChecks activé (ce qui est fortement recommandé), null et undefined ne peuvent être assignés qu'à eux-mêmes ou au type any. Pour les assigner à d'autres types, vous devez explicitement utiliser des union types (que nous verrons plus tard).

let u: undefined = undefined;
let n: null = null;

// Sans strictNullChecks:
let peutEtreChaine: string = null; // OK
let peutEtreNombre: number = undefined; // OK

// Avec strictNullChecks:
// let peutEtreChaine: string = null; // ERREUR
// let peutEtreNombre: number = undefined; // ERREUR

// Solution avec strictNullChecks (union type)
let nomOptionnel: string | null = "Alice";
nomOptionnel = null; // OK

never

Le type never représente le type de valeurs qui ne se produisent jamais. Il est utilisé pour les fonctions qui :

  • Retournent une erreur (et donc ne terminent jamais leur exécution normalement).
  • Contiennent une boucle infinie qui ne se termine jamais.
function lancerErreur(message: string): never {
    throw new Error(message);
}

function boucleInfinie(): never {
    while (true) {
        // ...
    }
}

object

Le type object représente tout ce qui n'est pas un type primitif (c'est-à-dire number, string, boolean, symbol, null, undefined, void). Il est rarement utilisé directement car il est trop générique. On préfère généralement utiliser des interfaces ou des types littéraux pour des objets plus spécifiques.

function processObject(obj: object): void {
    console.log("Ceci est un objet:", obj);
}

processObject({});
processObject([]);
processObject(new Date());
// processObject(123); // Erreur: Argument of type '123' is not assignable to parameter of type 'object'.

L'Inférence de Type

L'inférence de type est l'une des fonctionnalités les plus puissantes et conviviales de TypeScript. Elle permet à TypeScript de déduire le type d'une variable ou d'une expression sans que vous ayez à le spécifier explicitement. Cela réduit la verbosité du code tout en maintenant les avantages de la vérification de type.

Comment fonctionne l'inférence ?

TypeScript infère les types dans plusieurs situations courantes :

  1. Initialisation de variable : Lorsque vous déclarez une variable et l'initialisez immédiatement, TypeScript utilise la valeur d'initialisation pour déduire le type.

    let bonjour = "Hello World"; // TypeScript infère 'string'
    let count = 100;           // TypeScript infère 'number'
    let estValide = true;      // TypeScript infère 'boolean'
    
    // bonjour = 123; // Erreur: Type 'number' n'est pas assignable au type 'string'.
    

    Dans l'exemple ci-dessus, même si nous n'avons pas écrit : string, : number ou : boolean, TypeScript sait que bonjour est une chaîne de caractères, count est un nombre, et estValide est un booléen.

  2. Valeur de retour d'une fonction : TypeScript peut déduire le type de retour d'une fonction basée sur les types des valeurs retournées.

    function ajouter(a: number, b: number) {
        return a + b; // TypeScript infère que cette fonction retourne un 'number'
    }
    
    let resultat = ajouter(5, 3); // resultat est de type 'number'
    
  3. Tableaux et objets littéraux : Pour les tableaux, TypeScript infère un tableau de l'union des types des éléments. Pour les objets, il infère un type structurel basé sur les propriétés et leurs types.

    let couleurs = ["rouge", "vert", "bleu"]; // inféré comme `string[]`
    let valeurs = [1, "deux", true]; // inféré comme `(number | string | boolean)[]` (un tableau d'unions de types)
    
    let utilisateurInfo = {
        id: 1,
        nom: "Alice",
        actif: true
    };
    // inféré comme `{ id: number; nom: string; actif: boolean; }`
    

Bénéfices de l'Inférence de Type

  • Moins de verbiage : Vous n'avez pas besoin de spécifier explicitement les types partout, ce qui rend le code plus concis et plus rapide à écrire.
  • Lisibilité accrue : Un code moins encombré par des annotations de type répétitives peut être plus facile à lire pour des types simples et évidents.
  • Maintien de la sécurité des types : Malgré la concision, TypeScript continue de vérifier les types en arrière-plan, attrapant les erreurs potentielles avant l'exécution.

Quand faut-il utiliser l'annotation explicite ?

Bien que l'inférence de type soit très utile, il y a des cas où l'annotation explicite est préférable ou nécessaire :

  • Clarté et documentation : Pour les fonctions complexes, les paramètres ou les variables dont le type n'est pas immédiatement évident, l'annotation explicite peut servir de documentation supplémentaire.

  • Prévention d'erreurs d'inférence : Si vous initialisez une variable avec une valeur générique (par exemple, un tableau vide []), TypeScript peut inférer any[]. Dans ce cas, une annotation explicite est cruciale.

    let items = []; // inféré comme `any[]`
    items.push(1);
    items.push("deux"); // OK, mais peut causer des problèmes plus tard
    
    // Mieux avec annotation explicite:
    let numeros: number[] = []; // inféré comme `number[]`
    numeros.push(1);
    // numeros.push("deux"); // Erreur! Argument of type '"deux"' is not assignable to parameter of type 'number'.
    
  • Déclaration sans initialisation : Si vous déclarez une variable sans l'initialiser immédiatement, vous devez spécifier son type.

    let ageUtilisateur: number;
    // ageUtilisateur = "trente"; // Erreur: Type 'string' n'est pas assignable au type 'number'.
    ageUtilisateur = 30; // OK
    
  • API publiques : Pour les fonctions et les variables qui font partie d'une API publique (par exemple, des fonctions exportées d'un module), l'annotation explicite améliore la clarté pour les consommateurs de l'API.

Exemples Pratiques et Cas d'Usage

Voyons un exemple combinant plusieurs types de base et illustrant l'inférence de type.

// Déclaration avec inférence de type (valeur initiale)
let titreCours = "Maîtriser TypeScript"; // inféré comme 'string'
let dureeMinutes = 120; // inféré comme 'number'
let estDisponible = true; // inféré comme 'boolean'

// Déclaration explicite pour clarté ou initialisation différée
let tags: string[]; // Déclare 'tags' comme un tableau de strings
tags = ["TypeScript", "Web", "Développement"];

// Utilisation d'un tuple pour des données structurées
let formateur: [string, number, string] = ["Alice Dupont", 45, "Professeur"]; // Nom, âge, titre

// Utilisation d'une énumération pour des valeurs prédéfinies
enum NiveauDifficulte {
    Débutant = "DEBUTANT",
    Intermédiaire = "INTERMEDIAIRE",
    Avancé = "AVANCE"
}
let niveauActuel: NiveauDifficulte = NiveauDifficulte.Intermédiaire;

// Fonction avec paramètres et retour inférés ou annotés
function obtenirDescriptionCours(titre: string, duree: number): string {
    // Le type de retour est inféré comme 'string' car l'expression est une chaîne
    return `Le cours "${titre}" a une durée de ${duree} minutes.`;
}

// Fonction qui ne retourne rien (void)
function afficherNiveau(niveau: NiveauDifficulte): void {
    console.log(`Niveau de difficulté du cours : ${niveau}`);
}

// Exécution et vérification des types
console.log(obtenirDescriptionCours(titreCours, dureeMinutes));
// console.log(obtenirDescriptionCours(titreCours, "deux heures")); // Erreur: 'string' n'est pas assignable à 'number'.

afficherNiveau(niveauActuel);

// Accès aux éléments du tuple
console.log(`Formateur: ${formateur[0]}, Âge: ${formateur[1]}`);

// Tentative d'assignation d'un type incorrect
// dureeMinutes = "deux heures"; // Erreur: Type 'string' n'est pas assignable au type 'number'.

// Exemple où l'inférence peut être "trop" générique si pas d'initialisation
let listeEtudiants = []; // Inférence `any[]`
listeEtudiants.push("Jean"); // OK
listeEtudiants.push(123); // OK (car `any[]`)
console.log(listeEtudiants); // ['Jean', 123]

// Préférable: Annotation explicite pour un tableau vide
let notesExamens: number[] = []; // Inférence `number[]`
notesExamens.push(95); // OK
// notesExamens.push("excellent"); // Erreur: 'string' n'est pas assignable à 'number'.

Explication du code :

  • titreCours, dureeMinutes, estDisponible : Ces variables sont déclarées et immédiatement initialisées. TypeScript infère automatiquement leurs types (string, number, boolean) à partir des valeurs assignées.
  • tags: Ici, nous déclarons tags explicitement comme un string[] (tableau de chaînes de caractères). C'est utile si nous n'avons pas de valeur initiale ou si nous voulons être très clairs sur le type attendu.
  • formateur: Un tuple [string, number, string] est utilisé pour garantir que le premier élément est une chaîne, le second un nombre, et le troisième une chaîne, avec un nombre fixe d'éléments.
  • NiveauDifficulte: Une enum de type chaîne est utilisée pour définir des niveaux de difficulté prédéfinis, rendant le code plus lisible et moins sujet aux erreurs de frappe que des chaînes littérales.
  • obtenirDescriptionCours: Cette fonction prend un string et un number. TypeScript infère que la fonction retourne un string basé sur la valeur de retour (return \...``).
  • afficherNiveau: Cette fonction utilise void pour indiquer qu'elle n'a pas de valeur de retour significative.
  • Les lignes commentées (// console.log(obtenirDescriptionCours(titreCours, "deux heures"));) et (// dureeMinutes = "deux heures";) montrent comment TypeScript intercepte les erreurs de type avant l'exécution, grâce aux types inférés ou explicitement annotés.
  • L'exemple avec listeEtudiants versus notesExamens met en évidence pourquoi une annotation explicite est préférable pour les tableaux vides, car elle empêche TypeScript d'inférer any[], ce qui pourrait masquer des erreurs.

Conclusion et Résumé

Dans cette leçon, nous avons exploré les types de base fondamentaux en TypeScript, incluant number, string, boolean, array, tuple, enum, any, void, null, undefined, never et object. Chacun de ces types joue un rôle crucial dans la définition de la structure et du comportement attendu de nos données.

Nous avons également abordé l'inférence de type, une fonctionnalité clé de TypeScript qui permet au compilateur de déduire automatiquement les types de vos variables et expressions. Cette capacité réduit la quantité de code répétitif et améliore la lisibilité, tout en conservant les puissants avantages de la vérification de type.

Maîtriser les types de base et comprendre quand laisser TypeScript inférer les types ou quand les annoter explicitement est une compétence essentielle pour écrire du code TypeScript idiomatique, robuste et maintenable. Cela constitue la pierre angulaire pour la création d'applications web scalables et résistantes aux bugs.

Dans les leçons suivantes, nous approfondirons des concepts plus avancés comme les unions de types, les types littéraux, les interfaces, les types personnalisés et les classes, qui vous permettront de modéliser des structures de données encore plus complexes.