Leçon 4 / 8
Leçon 04 · TypeScript

Interfaces et types

Déclarer la forme d'un objet avec interface

En TypeScript, une interface décrit la structure attendue d'un objet : quelles propriétés il doit avoir et de quel type elles sont. C'est un contrat que l'objet doit respecter.

TypeScript — interface de base
interface User {
  id:     number;
  name:   string;
  email:  string;
  age?:   number;  // propriété optionnelle
}

const alice: User = {
  id:    1,
  name:  "Alice",
  email: "alice@exemple.fr"
  // age est optionnel, on peut l'omettre
};

const bob: User = {
  id:    2,
  name:  "Bob",
  email: "bob@exemple.fr",
  age:   30
};

Le point d'interrogation ? après le nom d'une propriété la rend optionnelle. Sans elle, la propriété est requise et TypeScript signale une erreur si elle manque.

Propriétés immuables avec readonly

Le modificateur readonly empêche la modification d'une propriété après l'initialisation de l'objet. Utile pour les identifiants, les constantes de configuration, etc.

TypeScript — readonly
interface Config {
  readonly apiUrl:  string;
  readonly version: number;
  timeout:          number;   // modifiable
}

const config: Config = {
  apiUrl:  "https://api.exemple.fr",
  version: 2,
  timeout: 5000
};

config.timeout = 10000;   // ✅ OK
config.version = 3;      // ❌ Erreur : Cannot assign to 'version'
config.apiUrl  = "...";  // ❌ Erreur : Cannot assign to 'apiUrl'
💡

readonly protège une propriété contre la réassignation, mais n'empêche pas la mutation d'un objet ou tableau imbriqué. Pour une immutabilité profonde, il faut d'autres approches.

Extension d'interface

Une interface peut étendre une autre avec le mot-clé extends. Elle hérite alors de toutes ses propriétés et peut en ajouter de nouvelles. Idéal pour modéliser des hiérarchies de données.

TypeScript — extends
interface User {
  id:    number;
  name:  string;
  email: string;
}

interface Admin extends User {
  role:        string;
  permissions: string[];
}

const superAdmin: Admin = {
  id:          1,
  name:        "Alice",
  email:       "alice@exemple.fr",
  role:        "superadmin",
  permissions: ["read", "write", "delete"]
};

On peut aussi étendre plusieurs interfaces à la fois : interface C extends A, B.

Alias de type avec type — vs interface

Le mot-clé type crée un alias de type. Il peut décrire un objet comme une interface, mais aussi des unions, des tuples ou n'importe quel type complexe.

TypeScript — interface vs type
// Interface — forme d'un objet
interface Point {
  x: number;
  y: number;
}

// Alias type — même résultat ici
type Point = {
  x: number;
  y: number;
};

// Mais type peut faire bien plus :
type Status = "actif" | "inactif" | "banni"; // union
type Pair   = [string, number];              // tuple
type Id     = number | string;              // union simple

Règle pratique : utilise interface pour modéliser la forme d'un objet ou d'une classe, et type pour les unions, les tuples et les alias de types primitifs. Les deux sont extensibles, mais seule interface supporte la déclaration de fusion (ajouter des propriétés à une interface existante dans un autre fichier).

Intersection types

L'opérateur & combine plusieurs types en un seul. L'objet résultant doit satisfaire tous les types à la fois. C'est l'équivalent type de l'extension d'interface.

TypeScript — intersection &
type User = {
  id:   number;
  name: string;
};

type Admin = {
  role:        string;
  permissions: string[];
};

type AdminUser = User & Admin;

const superAdmin: AdminUser = {
  id:          1,
  name:        "Alice",
  role:        "superadmin",
  permissions: ["read", "write"]
  // Toutes les propriétés des deux types sont requises
};

Index signatures

Une index signature permet de définir des objets dont les clés sont dynamiques mais dont le type des valeurs est connu. Pratique pour des dictionnaires ou des mappings.

TypeScript — index signature
// Toutes les valeurs sont des nombres
interface Scores {
  [key: string]: number;
}

const scores: Scores = {
  alice:   95,
  bob:     82,
  charlie: 77
};

scores.diana = 90; // ✅ OK — n'importe quelle clé string
scores.eve   = "très bien"; // ❌ Erreur — valeur doit être number

// Combiner propriétés fixes et index signature
interface Catalogue {
  version:           number;          // propriété fixe
  [produit: string]: number | string; // doit accepter le type de version aussi
}
⚠️

Quand tu combines une propriété fixe avec une index signature, le type de la propriété fixe doit être compatible avec le type de l'index signature. C'est pourquoi version: number impose number | string pour l'index dans l'exemple ci-dessus.

Type guards

Un type guard est une vérification qui permet à TypeScript de savoir précisément quel type on manipule dans un bloc de code. Il existe trois approches principales.

TypeScript — typeof, instanceof, is
// 1. typeof — pour les types primitifs
function afficher(valeur: string | number): string {
  if (typeof valeur === "string") {
    return valeur.toUpperCase(); // ici valeur est string
  }
  return valeur.toFixed(2);       // ici valeur est number
}

// 2. instanceof — pour les classes
function traiter(err: Error | string): void {
  if (err instanceof Error) {
    console.log(err.message); // err est un Error
  } else {
    console.log(err);         // err est une string
  }
}

// 3. Custom type guard avec "is"
interface Cat  { paws: number; meow(): void; }
interface Dog  { paws: number; bark(): void; }

function isCat(animal: Cat | Dog): animal is Cat {
  return "meow" in animal;
}

function faireParler(animal: Cat | Dog): void {
  if (isCat(animal)) {
    animal.meow(); // TypeScript sait que c'est un Cat
  } else {
    animal.bark(); // TypeScript sait que c'est un Dog
  }
}

Discriminated unions

Les discriminated unions (unions discriminantes) sont un pattern très puissant pour modéliser des états ou des événements différents dans une seule union. Chaque membre possède une propriété discriminante (souvent appelée type ou kind) avec une valeur littérale unique.

TypeScript — discriminated union
// Chaque type a une propriété "kind" unique
interface Cercle {
  kind:   "cercle";
  rayon:  number;
}

interface Rectangle {
  kind:     "rectangle";
  largeur:  number;
  hauteur:  number;
}

interface Triangle {
  kind:   "triangle";
  base:   number;
  hauteur: number;
}

type Forme = Cercle | Rectangle | Triangle;

function aire(forme: Forme): number {
  switch (forme.kind) {
    case "cercle":
      return Math.PI * forme.rayon ** 2;
    case "rectangle":
      return forme.largeur * forme.hauteur;
    case "triangle":
      return (forme.base * forme.hauteur) / 2;
  }
}

console.log(aire({ kind: "cercle",    rayon: 5 }));          // 78.54
console.log(aire({ kind: "rectangle", largeur: 4, hauteur: 6 })); // 24

TypeScript vérifie l'exhaustivité du switch : si tu ajoutes un nouveau membre à l'union sans l'ajouter au switch, tu obtiens une erreur de compilation.

// À retenir
  • interface décrit la forme d'un objet ; ? rend une propriété optionnelle
  • readonly empêche la réassignation d'une propriété après initialisation
  • interface B extends A hérite toutes les propriétés de A
  • type peut tout faire que interface + unions, tuples, alias primitifs
  • type C = A & B combine les deux types (intersection)
  • Index signature [key: string]: number pour les objets à clés dynamiques
  • Type guards (typeof, instanceof, is) permettent à TS de rétrécir le type
  • Discriminated unions = union avec propriété littérale unique par membre