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.
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.
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.
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.
// 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.
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.
// 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.
// 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.
// 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.
interfacedécrit la forme d'un objet ;?rend une propriété optionnellereadonlyempêche la réassignation d'une propriété après initialisationinterface B extends Ahérite toutes les propriétés de Atypepeut tout faire queinterface+ unions, tuples, alias primitifstype C = A & Bcombine les deux types (intersection)- Index signature
[key: string]: numberpour 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