Types composés#
Les types primitifs que nous avons vus au chapitre précédent sont les atomes du système de types. Les types composés en sont les molécules : ils permettent de structurer des données complexes, de définir des contrats entre composants et d’exprimer des relations entre types. C’est là que TypeScript révèle toute sa puissance pour modéliser avec précision le domaine d’une application.
Tableaux et tuples#
Tableaux#
Il existe deux syntaxes équivalentes pour typer un tableau en TypeScript. La première utilise le type de l’élément suivi de crochets, la seconde utilise la forme générique Array<T> :
// Syntaxe avec crochets (la plus courante)
const prénoms: string[] = ["Alice", "Bob", "Clara"];
const notes: number[] = [18, 15, 20, 12];
// Syntaxe générique (équivalente)
const prénoms2: Array<string> = ["Alice", "Bob", "Clara"];
const notes2: Array<number> = [18, 15, 20, 12];
Les deux formes sont strictement interchangeables. La convention dans la communauté TypeScript est de préférer la syntaxe T[] pour les types simples et Array<T> pour les types complexes ou les génériques imbriqués, pour des raisons de lisibilité.
Tableaux en lecture seule#
Le modificateur readonly appliqué à un tableau empêche toute modification du tableau après sa création : on ne peut plus appeler push, pop, splice ni aucune méthode mutante.
const couleurs: readonly string[] = ["rouge", "vert", "bleu"];
// Équivalent : ReadonlyArray<string>
couleurs.push("jaune"); // Erreur : Property 'push' does not exist
couleurs[0] = "violet"; // Erreur : Index signature in type 'readonly string[]'
// only permits reading.
Tuples#
Un tuple est un tableau de longueur fixe dont chaque position a un type précisément défini. Contrairement à un tableau ordinaire, un tuple encode à la fois l’ordre et les types de ses éléments :
// Un tuple de deux éléments : une chaîne et un nombre
type Point2D = [number, number];
const origine: Point2D = [0, 0];
// Un tuple hétérogène
type EntréeJournal = [Date, string, 'info' | 'avert' | 'erreur'];
const événement: EntréeJournal = [new Date(), "Connexion réussie", 'info'];
// Déstructuration d'un tuple
const [date, message, niveau] = événement;
Définition 2 (Tuple)
Un tuple est un type tableau de longueur fixe dans lequel le type de chaque élément est connu à chaque position. Les tuples permettent de regrouper des valeurs hétérogènes sans créer un objet nommé, tout en bénéficiant d’une vérification de types stricte sur chaque position. Ils sont particulièrement utiles pour représenter des paires, des triplets ou les valeurs de retour de fonctions qui renvoient plusieurs résultats.
Tuples optionnels et rest#
TypeScript permet des tuples avec des éléments optionnels (marqués ?) et des éléments rest (spreads variادiques) :
// Élément optionnel en fin de tuple
type PaireAvecLabel = [string, string, string?];
const p1: PaireAvecLabel = ["clé", "valeur"]; // OK
const p2: PaireAvecLabel = ["clé", "valeur", "label"]; // OK
// Élément rest : capture un nombre variable d'éléments
type MinMaxEtAutres = [number, number, ...number[]];
const données: MinMaxEtAutres = [1, 99, 12, 45, 67, 8];
Interfaces#
Une interface définit le contrat qu’un objet doit respecter : quelles propriétés il possède et de quel type elles sont. C’est l’un des construits de TypeScript les plus utilisés pour modéliser des structures de données.
interface Utilisateur {
id: number;
nom: string;
email: string;
}
const alice: Utilisateur = {
id: 1,
nom: "Alice Dupont",
email: "alice@exemple.fr",
};
Propriétés optionnelles et readonly#
Le modificateur ? après un nom de propriété la rend optionnelle : elle peut être absente de l’objet. Le modificateur readonly interdit toute réassignation après la création de l’objet :
interface Produit {
readonly id: number; // Ne peut pas être modifié après création
nom: string;
description?: string; // Optionnelle : peut être absente
prix: number;
stock?: number; // Optionnelle
}
const livre: Produit = {
id: 42,
nom: "TypeScript en profondeur",
prix: 34.90,
};
livre.id = 99; // Erreur : Cannot assign to 'id' because it is a read-only property.
Extension d’interfaces avec extends#
Une interface peut en étendre une ou plusieurs autres, héritant de toutes leurs propriétés :
interface Personne {
nom: string;
âge: number;
}
interface Employé extends Personne {
entreprise: string;
poste: string;
}
interface Cadre extends Employé {
équipe: string[];
budget: number;
}
const directeur: Cadre = {
nom: "Bernard Martin",
âge: 48,
entreprise: "Acme Corp",
poste: "Directeur Technique",
équipe: ["Alice", "Bob", "Clara"],
budget: 500_000,
};
Fusion de déclarations (declaration merging)#
L’une des propriétés spécifiques aux interfaces (et qui les distingue des alias de types) est la fusion de déclarations : si on déclare deux interfaces avec le même nom dans la même portée, TypeScript les fusionne automatiquement en une seule interface combinant toutes les propriétés.
interface Fenêtre {
titre: string;
}
interface Fenêtre {
largeur: number;
hauteur: number;
}
// TypeScript voit : { titre: string; largeur: number; hauteur: number; }
const fenêtre: Fenêtre = { titre: "Accueil", largeur: 1920, hauteur: 1080 };
Cette fonctionnalité est utilisée pour augmenter des interfaces définies dans des bibliothèques externes (en ajoutant des propriétés à une interface existante dans un fichier .d.ts).
Alias de types (type)#
Le mot-clé type permet de créer un alias qui donne un nom à n’importe quel type, y compris des unions, des intersections, des types fonctionnels et des types génériques :
type Identifiant = number | string;
type Callback = (erreur: Error | null, résultat?: string) => void;
type PaireDeStrings = [string, string];
type Dictionnaire<V> = Record<string, V>;
type vs interface : quand choisir l’un ou l’autre ?#
Cette question revient fréquemment. Voici une synthèse pratique.
Remarque 5
Préférer interface pour :
Définir la forme d’objets et de classes, surtout dans les API publiques
Tirer parti de la fusion de déclarations (augmentation de bibliothèques)
Exprimer l’héritage via
extends
Préférer type pour :
Les unions et les intersections :
type Résultat = Succès | ÉchecLes types primitifs, les tuples, les types mappés et conditionnels
Les aliases de types complexes (fonctions, génériques avancés)
En pratique, les deux sont souvent interchangeables pour les objets simples. La recommandation officielle de l’équipe TypeScript est d’utiliser interface par défaut et de se tourner vers type lorsqu’on a besoin de fonctionnalités qu”interface ne supporte pas (unions, types conditionnels, etc.).
// Avec interface
interface Point { x: number; y: number; }
// Avec type (équivalent pour un objet simple)
type Point2 = { x: number; y: number; };
// Seul type peut exprimer ceci :
type IDOuNom = number | string;
type Résultat<T> = { données: T } | { erreur: string };
Types union et intersection#
Union (A | B)#
Un type union exprime qu’une valeur peut être de l’un ou l’autre des types listés. C’est l’équivalent logique du « ou » :
type IDOuNom = number | string;
function chercher(id: IDOuNom): Utilisateur | undefined {
if (typeof id === 'number') {
return baseDeDonnées.trouverParId(id);
} else {
return baseDeDonnées.trouverParNom(id);
}
}
TypeScript exige qu’on vérifie le type avant d’utiliser une propriété qui n’existe que sur l’un des membres de l’union. C’est le narrowing que nous avons vu au chapitre précédent.
Intersection (A & B)#
Un type intersection exprime qu’une valeur doit avoir toutes les propriétés des types combinés. C’est l’équivalent logique du « et » :
interface AvecHorodatage {
crééLe: Date;
modifiéLe: Date;
}
interface Document {
titre: string;
contenu: string;
}
type DocumentAudit = Document & AvecHorodatage;
const rapport: DocumentAudit = {
titre: "Rapport annuel",
contenu: "...",
crééLe: new Date("2024-01-01"),
modifiéLe: new Date("2024-03-15"),
};
Unions discriminées (discriminated unions)#
Les unions discriminées sont un patron de conception très puissant en TypeScript. L’idée est d’avoir plusieurs types dans une union, tous partageant une propriété discriminante de type littéral (souvent nommée kind, type ou tag). TypeScript peut alors affiner précisément le type en fonction de la valeur de cette propriété.
Définition 3 (Union discriminée)
Une union discriminée (ou tagged union) est un type union dont chaque membre possède une propriété commune de type littéral — le discriminant — qui permet à TypeScript d’identifier de façon certaine quel membre de l’union est présent. Grâce à ce discriminant, TypeScript peut narrower automatiquement le type dans chaque branche d’un if ou d’un switch, sans avoir recours à instanceof ou à des vérifications de propriétés supplémentaires.
interface ChargementEnCours {
état: 'chargement';
}
interface ChargementRéussi {
état: 'succès';
données: string[];
}
interface ChargementÉchoué {
état: 'erreur';
messageErreur: string;
codeHTTP: number;
}
type ÉtatChargement =
| ChargementEnCours
| ChargementRéussi
| ChargementÉchoué;
function afficher(état: ÉtatChargement): string {
switch (état.état) {
case 'chargement':
return "Chargement en cours…";
case 'succès':
// Ici, TypeScript sait que état : ChargementRéussi
return `${état.données.length} éléments chargés.`;
case 'erreur':
// Ici, TypeScript sait que état : ChargementÉchoué
return `Erreur ${état.codeHTTP} : ${état.messageErreur}`;
}
}
Visualisation : unions discriminées#
Types littéraux#
Un type littéral est un type dont l’ensemble des valeurs possibles se réduit à une seule valeur précise. TypeScript supporte les littéraux de chaîne, de nombre et de booléen :
type Direction = 'nord' | 'sud' | 'est' | 'ouest';
type DéFace = 1 | 2 | 3 | 4 | 5 | 6;
type Vrai = true;
function déplacer(direction: Direction, pas: number): void {
// direction ne peut être que 'nord', 'sud', 'est' ou 'ouest'
}
déplacer('nord', 10); // OK
déplacer('haut', 5); // Erreur : '"haut"' is not assignable to type 'Direction'
Les types littéraux sont particulièrement puissants combinés aux unions discriminées et à as const (vu au chapitre précédent).
Exemple 2 (Types littéraux pour des états d’interface)
Les types littéraux permettent de modéliser précisément les états possibles d’un composant d’interface :
type ÉtatBouton = 'actif' | 'désactivé' | 'chargement' | 'erreur';
type Taille = 'sm' | 'md' | 'lg' | 'xl';
type Variant = 'primaire' | 'secondaire' | 'danger' | 'fantôme';
interface PropsBouton {
état: ÉtatBouton;
taille: Taille;
variant: Variant;
libellé: string;
onClick?: () => void;
}
function rendreBouton(props: PropsBouton): void {
// TypeScript garantit que props.état est toujours l'une des 4 valeurs
// TypeScript garantit que props.taille est toujours l'une des 4 tailles
}
## Énumérations (`enum`)
Les **énumérations** permettent de définir un ensemble nommé de constantes. TypeScript en supporte plusieurs variantes.
### Énumérations numériques
Par défaut, une énumération TypeScript est **numérique** : chaque membre reçoit une valeur entière incrémentale à partir de 0 :
```typescript
enum Direction {
Nord, // 0
Sud, // 1
Est, // 2
Ouest, // 3
}
const cap: Direction = Direction.Nord;
console.log(cap); // 0
console.log(Direction[0]); // "Nord" (accès inverse)
```
On peut choisir la valeur de départ ou attribuer des valeurs manuellement :
```typescript
enum CodeHTTP {
OK = 200,
CréationOK = 201,
NonTrouvé = 404,
ErreurServeur = 500,
}
```
### Énumérations de chaînes
Une **énumération de chaînes** attribue une valeur chaîne à chaque membre. Contrairement aux énumérations numériques, les valeurs sont lisibles dans les logs et les données sérialisées :
```typescript
enum Statut {
EnAttente = 'EN_ATTENTE',
EnCours = 'EN_COURS',
Terminé = 'TERMINÉ',
Annulé = 'ANNULÉ',
}
const s: Statut = Statut.EnCours;
console.log(s); // "EN_COURS"
```
### `const enum`
Un `const enum` est **effacé à la compilation** : TypeScript remplace chaque utilisation par sa valeur littérale, sans générer de code d'objet JavaScript :
```typescript
const enum Touche {
Entrée = 13,
Échap = 27,
Espace = 32,
}
if (event.keyCode === Touche.Entrée) { /* ... */ }
// Compilé en : if (event.keyCode === 13) { ... }
```
### `enum` vs unions de types littéraux
```{prf:remark}
:label: remark-03-02
Les `enum` présentent plusieurs inconvénients qui poussent une partie de la communauté TypeScript à leur préférer les **unions de types littéraux** :
- Les `enum` génèrent du code JavaScript à l'exécution (sauf `const enum`), ce qui alourdit le bundle.
- Les énumérations numériques acceptent silencieusement n'importe quel nombre : `maFonction(42)` passe sans erreur même si `42` ne correspond à aucun membre.
- L'accès inverse (`Direction[0]`) peut provoquer des surprises.
**Alternative avec des unions de littéraux :**
```typescript
type Direction = 'Nord' | 'Sud' | 'Est' | 'Ouest';
// Ou avec un objet constant pour conserver un namespace :
const Direction = {
Nord: 'Nord',
Sud: 'Sud',
Est: 'Est',
Ouest: 'Ouest',
} as const;
type Direction = typeof Direction[keyof typeof Direction];
```
Cette approche produit du JavaScript ordinaire, sans code supplémentaire, avec une vérification de types stricte et un typage structurel compatible avec l'écosystème.
```
## Résumé
Dans ce chapitre, nous avons construit une palette complète de types composés :
- Les **tableaux** (`T[]` ou `Array<T>`) et les **tuples** (`[T1, T2, ...]`) permettent de structurer des séquences de valeurs. `readonly` interdit les modifications après création.
- Les **interfaces** définissent des contrats d'objets, avec support des propriétés optionnelles (`?`), `readonly`, l'héritage via `extends` et la fusion de déclarations.
- Les **alias de types** (`type`) permettent de nommer n'importe quel type, y compris des unions et des types complexes. On préfère `interface` pour les objets dans les API publiques et `type` pour les unions et les construits avancés.
- Les **unions** (`A | B`) et les **intersections** (`A & B`) combinent des types. Les **unions discriminées** avec un champ littéral commun sont un patron fondamental pour modéliser des états finis, permettant à TypeScript de narrower automatiquement le type dans chaque branche.
- Les **types littéraux** restreignent un type à une valeur exacte. Combinés aux unions, ils forment des ensembles finis de valeurs nommées.
- Les **énumérations** (`enum`) offrent des constantes nommées groupées, mais les unions de types littéraux avec `as const` sont souvent préférées pour leur transparence et leur légèreté à l'exécution.
Dans le chapitre suivant, nous nous concentrerons sur les **fonctions** : annotation des paramètres et du retour, paramètres optionnels, surcharges, génériques, gestion de `this` et types de fonctions avancés.