Cours JavaScript et TypeScript · Module 6: Bases de TypeScript

Les Utility Types de TypeScript

7 min de lecture
Dans cette leçon10

Le Dr Henry Wu passe en revue la base de données génétique de Jurassic Park. « On a une interface Dinosaur avec 15 propriétés », dit-il, « mais pour le formulaire de mise à jour, je n'ai besoin que de 5, et pour l'aperçu, de 3 seulement. Est-ce que je dois créer une interface séparée pour chaque cas ? » Copier des interfaces a un coût caché : quand un nouveau champ s'ajoute à Dinosaur, il faut corriger chaque copie à la main, et quelqu'un en oublie toujours une. Heureusement, TypeScript propose les Utility Types (types utilitaires), c'est-à-dire des transformations toutes faites qui créent de nouveaux types à partir de types existants.

Que sont les utility types ?

Les utility types sont des types génériques intégrés à TypeScript qui transforment des types existants en nouveaux types. Tu passes le type à transformer entre chevrons, comme l'argument des fonctions génériques des leçons précédentes. Inutile de les importer, et ils disparaissent après la compilation, car ils n'existent qu'au moment de la vérification des types. Au lieu d'interfaces séparées, tu as un seul type de base dont tu dérives des variantes, comme des lignées d'élevage issues d'un seul échantillon d'ADN.

Partial<T> - toutes les propriétés optionnelles

Dans les exemples, nous utiliserons une interface simplifiée à cinq champs. Le champ dietType accepte l'une de trois valeurs, écrites sous forme d'union de littéraux. Partial<T> rend toutes les propriétés du type T optionnelles. C'est le type idéal pour une mise à jour, où l'on ne veut pas exiger tous les champs :

1interface Dinosaur {
2  name: string;
3  species: string;
4  dangerLevel: number;
5  enclosureId: string;
6  dietType: 'herbivore' | 'carnivore' | 'omnivore';
7}
8
9// Partial<Dinosaur> = tous les champs optionnels
10type DinosaurUpdate = Partial<Dinosaur>;
11
12// On peut maintenant passer uniquement les champs modifiés
13function updateDinosaur(id: string, changes: DinosaurUpdate): void {
14  console.log(`Mise à jour du dinosaure ${id} :`, changes);
15}
16
17// Correct - on ne passe que ce qu'on modifie
18updateDinosaur('DINO-001', { dangerLevel: 9 });
19updateDinosaur('DINO-002', { name: 'Blue', enclosureId: 'E-7' });

L'interface Dinosaur elle-même n'a pas changé, car Partial crée un nouveau type à côté d'elle. La vérification fonctionne toujours : une faute de frappe comme dangerLvl sera signalée. Partial est superficiel, donc les champs d'un objet imbriqué restent obligatoires.

Required<T> - toutes les propriétés obligatoires

Required<T> fait l'inverse de Partial : il supprime le point d'interrogation, donc toutes les propriétés, même optionnelles, deviennent obligatoires :

1interface DinosaurConfig {
2  name: string;
3  trackingChip?: boolean;  // optionnel
4  feedingSchedule?: string; // optionnel
5}
6
7// Required<DinosaurConfig> = tout est obligatoire
8type CompleteDinosaurConfig = Required<DinosaurConfig>;
9
10// Maintenant, trackingChip et feedingSchedule sont aussi obligatoires
11const config: CompleteDinosaurConfig = {
12  name: 'Rex',
13  trackingChip: true,        // doit être fourni
14  feedingSchedule: '08:00'   // doit être fourni
15};

S'il manquait feedingSchedule, le compilateur signalerait la propriété manquante. C'est utile quand une configuration doit être complète une fois les valeurs par défaut remplies.

Pick<T, K> - sélectionner des propriétés

Pick<T, K> crée un nouveau type qui ne contient que les propriétés choisies de T. Le deuxième paramètre, ce sont les noms des champs reliés par le signe | :

1// Seulement name et species, tirés de l'interface Dinosaur complète
2type DinosaurPreview = Pick<Dinosaur, 'name' | 'species'>;
3
4// DinosaurPreview = { name: string; species: string; }
5
6const preview: DinosaurPreview = {
7  name: 'Blue',
8  species: 'Velociraptor'
9};
10
11// Utile par exemple pour une liste affichée à l'écran
12function renderDinosaurList(dinosaurs: DinosaurPreview[]): void {
13  dinosaurs.forEach(dino => {
14    console.log(`${dino.name} (${dino.species})`);
15  });
16}

Pick vérifie que les clés indiquées existent : Pick<Dinosaur, 'enclosureID'> avec une faute de frappe se termine tout de suite par une erreur de compilation. Les types des champs passent sans changement, donc name reste une chaîne.

Omit<T, K> - omettre des propriétés

Omit<T, K> fait l'inverse de Pick et crée un nouveau type sans les propriétés choisies :

1// Dinosaur sans enclosureId (par exemple pour une espèce tout juste découverte)
2type DinosaurWithoutEnclosure = Omit<Dinosaur, 'enclosureId'>;
3
4const newDiscovery: DinosaurWithoutEnclosure = {
5  name: 'Nouvelle espèce',
6  species: 'Unknownus rex',
7  dangerLevel: 5,
8  dietType: 'carnivore'
9  // pas besoin de fournir enclosureId
10};
11
12// Omit de plusieurs champs à la fois
13type DinosaurBasicInfo = Omit<Dinosaur, 'enclosureId' | 'dangerLevel'>;

Omit ne supprime rien des objets existants, il décrit seulement une nouvelle forme. Il cache aussi un piège : contrairement à Pick, il ne vérifie pas si la clé existe, donc Omit<Dinosaur, 'enclosureID'> passe sans erreur et n'omet rien. C'est pourquoi, quand tu ne gardes que quelques champs, je te recommande Pick.

Record<K, V> - dictionnaire typé

Record<K, V> crée un type d'objet dont les clés sont de type K et les valeurs de type V :

1// Dictionnaire : nom de la zone -> liste des dinosaures
2type ZoneMap = Record<string, Dinosaur[]>;
3
4const parkZones: ZoneMap = {
5  'Zone-A': [{ name: 'Rex', species: 'T-Rex', dangerLevel: 10, enclosureId: 'E-1', dietType: 'carnivore' }],
6  'Zone-B': [{ name: 'Trike', species: 'Triceratops', dangerLevel: 3, enclosureId: 'E-2', dietType: 'herbivore' }]
7};
8
9// Record avec des clés limitées (union type)
10type DietCount = Record<'herbivore' | 'carnivore' | 'omnivore', number>;
11
12const census: DietCount = {
13  herbivore: 45,
14  carnivore: 12,
15  omnivore: 8
16};

Avec une clé string, le dictionnaire accepte n'importe quel nom de zone. Avec une union de clés, chacune d'elles est obligatoire, donc s'il manquait omnivore dans le recensement, le compilateur signalerait une erreur. C'est un excellent moyen de n'oublier aucune catégorie.

Readonly<T> - type immuable

Readonly<T> rend toutes les propriétés accessibles en lecture seule :

1type FrozenDinosaur = Readonly<Dinosaur>;
2
3const frozenDNA: FrozenDinosaur = {
4  name: 'Amber Specimen',
5  species: 'Unknown',
6  dangerLevel: 0,
7  enclosureId: 'LAB-1',
8  dietType: 'herbivore'
9};
10
11// frozenDNA.name = 'Changed'; // Erreur ! Cannot assign to 'name' because it is a read-only property

Contrairement à Object.freeze(), vu dans la leçon sur les prototypes, cette protection ne fonctionne qu'à la compilation. Dans le JavaScript qui s'exécute, l'objet reste un objet ordinaire. Readonly est lui aussi superficiel et ne protège pas les objets imbriqués.

Combiner les utility types

La véritable puissance apparaît quand on combine plusieurs utility types. Lis ces écritures de l'intérieur vers l'extérieur :

1// Formulaire de création d'un dinosaure : tous les champs obligatoires SAUF enclosureId
2type CreateDinosaurForm = Omit<Dinosaur, 'enclosureId'> & {
3  enclosureId?: string; // optionnel à la création
4};
5
6// Formulaire de modification : seulement les champs choisis, tous optionnels
7type EditDinosaurForm = Partial<Pick<Dinosaur, 'name' | 'dangerLevel' | 'enclosureId'>>;
8
9// Aperçu immuable avec les champs choisis
10type DinosaurReadonlyPreview = Readonly<Pick<Dinosaur, 'name' | 'species' | 'dangerLevel'>>;
11
12const readonlyPreview: DinosaurReadonlyPreview = {
13  name: 'Blue',
14  species: 'Velociraptor',
15  dangerLevel: 8
16};
17// readonlyPreview.dangerLevel = 10; // Erreur !

EditDinosaurForm choisit d'abord trois champs, puis les rend optionnels. Dans CreateDinosaurForm, le signe & est une intersection : il combine le type sans enclosureId avec un objet dans lequel ce champ est optionnel.

Résumé

Les utility types de TypeScript permettent de créer des variantes de types existants sans dupliquer le code :

TypeAction
Partial<T>Tous les champs optionnels
Required<T>Tous les champs obligatoires
Pick<T, K>Sélectionner des champs précis
Omit<T, K>Omettre des champs précis
Record<K, V>Dictionnaire typé
Readonly<T>Tous les champs en lecture seule

Ce n'est pas toute la boîte à outils : la documentation de TypeScript décrit aussi, entre autres, ReturnType, Parameters, Exclude et NonNullable. Comme le dit le Dr Wu : « Ne crée pas 10 interfaces séparées quand un seul type de base et les utility types règlent l'affaire avec élégance et en toute sécurité ! » Dans la prochaine leçon, nous nous occuperons de la configuration de TypeScript et des outils qui entourent le compilateur.

Pratique

Dans le bac à sable t'attend l'interface Dinosaur avec des exemples de Partial et de Pick. Ajoute une variante avec Omit et fais exprès une faute de frappe dans une clé, pour voir la différence entre Pick et Omit.

Code de cette leçon : index.ts
1// TypeScript Utility Types
2// Système de gestion des dinosaurs dans le Parc Jurassique
3
4// Interface de base du dinosaure
5interface Dinosaur {
6  name: string;
7  species: string;
8  dangerLevel: number;
9  enclosureId: string;
10  dietType: "herbivore" | "carnivore" | "omnivore";
11}
12
13// === Partial<T> - tous les champs optionnels ===
14type DinosaurUpdate = Partial<Dinosaur>;
15
16function updateDinosaur(id: string, changes: DinosaurUpdate): void {
17  console.log("Mise à jour du dinosaure " + id + ":", changes);
18}
19
20updateDinosaur("DINO-001", { dangerLevel: 9 });
21updateDinosaur("DINO-002", { name: "Blue", enclosureId: "E-7" });
22
23// === Pick<T, K> - sélectionne des properties ===
24type DinosaurPreview = Pick<Dinosaur, "name" | "species">;
25
26const preview: DinosaurPreview = {
27  name: "Blue",
28  species: "Velociraptor",
29};
30console.log("\nAperçu:", preview);
31
32// === Omit<T, K> - omet des properties ===
33type DinosaurWithoutEnclosure = Omit<Dinosaur, "enclosureId">;
34
35const newDiscovery: DinosaurWithoutEnclosure = {
36  name: "Nouvelle espèce",
37  species: "Unknownus rex",
38  dangerLevel: 5,
39  dietType: "carnivore",
40};
41console.log("\nNouvelle découverte:", newDiscovery);
42
43// === Record<K, V> - dictionnaire typé ===
44type DietCount = Record<"herbivore" | "carnivore" | "omnivore", number>;
45
46const census: DietCount = {
47  herbivore: 45,
48  carnivore: 12,
49  omnivore: 8,
50};
51console.log("\nRecensement:", census);
52
53// === Readonly<T> - type immuable ===
54type FrozenDinosaur = Readonly<Dinosaur>;
55
56const frozen: FrozenDinosaur = {
57  name: "Amber Specimen",
58  species: "Unknown",
59  dangerLevel: 0,
60  enclosureId: "LAB-1",
61  dietType: "herbivore",
62};
63console.log("\nFigé:", frozen);
64// frozen.name = "Changed"; // Erreur ! Read-only
65
66// === Combinaison de utility types ===
67type EditForm = Partial<Pick<Dinosaur, "name" | "dangerLevel" | "enclosureId">>;
68
69const edit: EditForm = { dangerLevel: 7 };
70console.log("\nFormulaire d'édition:", edit);

Retiens bien : le type de base est l'échantillon d'ADN d'origine, et les utility types en élèvent des variétés, donc une correction apportée à l'échantillon atteint toutes les lignées à la fois.

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Quel type utilitaire en TypeScript rend TOUTES les propriétés d’un objet optionnelles ?

  2. 2. Que fait le type Omit<T, K> en TypeScript ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Éditeur de code

    Utilise Pick, Omit et Partial sur l’interface Dinosaur pour créer des variantes de type.

  • Tri horizontal

    Ordonne les éléments de l’utilisation du type utilitaire Pick :

  • Tri vertical

    Ordonne les concepts TypeScript du plus simple au plus complexe :

Articles utiles