Retour au cours

frontend / typescript

Utility Types

Leçon 81 exercice

Explication

Ce que vous allez apprendre

  • Dériver des variantes d'un type existant avec Partial, Required, Readonly
  • Sélectionner ou exclure des clés précises avec Pick et Omit
  • Construire un dictionnaire typé clé-valeur avec Record
  • Extraire un type depuis une fonction existante avec ReturnType, Parameters, Awaited
  • Composer plusieurs utility types entre eux (Partial<Omit<T, "id">>)

Dans quel contexte ?

Une équipe maintient une interface Utilisateur avec 8 champs, et a dupliqué manuellement une variante UtilisateurSansMotDePasse pour l'API publique, une autre UtilisateurMiseAJour avec tous les champs optionnels pour un PATCH. Quand un nouveau champ telephone est ajouté à Utilisateur, les deux variantes dupliquées ne sont pas mises à jour, et l'API publique continue de ne pas exposer ce champ alors qu'elle le devrait. Remplacer ces duplications par Omit<Utilisateur, "motDePasse"> et Partial<Omit<Utilisateur, "id">> garantit que toute modification du type source se répercute automatiquement partout.

Ne jamais réécrire un type à la main

Une fois qu'un type comme Utilisateur est défini, il est extrêmement fréquent d'avoir besoin de variantes : une version où tous les champs sont optionnels pour une mise à jour partielle, une version sans le mot de passe pour l'afficher côté client, une version qui ne garde que l'identifiant et le nom. Réécrire ces variantes à la main serait source d'erreurs et de désynchronisation dès que le type d'origine change.

Prérequis

Cette leçon suppose les interfaces (leçon 3) bien maîtrisées : chaque utility type présenté ici prend un type existant en entrée et en dérive un nouveau.

Des transformations prêtes à l'emploi

TypeScript fournit une boîte à outils de types utilitaires, déjà intégrée au langage, qui dérive un nouveau type à partir d'un type existant : Partial rend tout optionnel, Required rend tout obligatoire, Readonly empêche toute réassignation, Pick et Omit sélectionnent ou excluent des clés précises, Record construit un dictionnaire typé clé-valeur.

Utility typeEffet
Partial<T>Toutes les propriétés deviennent optionnelles
Required<T>Toutes les propriétés deviennent obligatoires
Readonly<T>Toutes les propriétés deviennent non réassignables
Pick<T, K>Ne garde que les clés listées dans K
Omit<T, K>Exclut les clés listées dans K
Record<K, V>Construit un dictionnaire avec des clés K et des valeurs V

Extraire des types depuis du code existant

D'autres utilitaires travaillent dans l'autre sens : au lieu de partir d'un type pour en dériver un autre, ils extraient un type directement depuis une fonction déjà écrite. ReturnType récupère ce qu'une fonction retourne, Parameters récupère ses arguments sous forme de tuple, Awaited déballe le contenu d'une Promise. Cette approche garantit que la signature et les types dérivés restent toujours synchronisés : si la fonction change, les types qui en dépendent changent automatiquement avec elle.

Astuce

Avant de créer manuellement une interface UtilisateurSansMotDePasse, demandez-vous toujours si Omit<Utilisateur, "motDePasse"> ne suffirait pas : ça évite une désynchronisation future entre les deux types.

Composer plutôt que dupliquer

Le vrai gain de ces utilitaires apparaît quand on les combine, comme Partial<Omit<Utilisateur, "id">>. C'est cette capacité de composition qui distingue un typage TypeScript mature d'un typage qui se contente de dupliquer des interfaces légèrement différentes partout dans le code.

Commandes & code

Utility Types

ts
interface Utilisateur {
  id: number;
  nom: string;
  email: string;
  motDePasse: string;
}

// Partial<T> : toutes les propriétés deviennent optionnelles (utile pour un PATCH)
function mettreAJour(id: number, patch: Partial<Utilisateur>) {
  /* ... */
}
mettreAJour(1, { nom: "Nouveau nom" });

// Required<T> : l'inverse, tout devient obligatoire
type UtilisateurComplet = Required<Partial<Utilisateur>>;

// Readonly<T> : empêche toute réassignation de propriété
const config: Readonly<{ apiUrl: string }> = { apiUrl: "https://api.example.com" };
// config.apiUrl = "autre"; // Error : lecture seule

// Pick<T, K> : ne garde que certaines clés
type UtilisateurPublic = Pick<Utilisateur, "id" | "nom">;

// Omit<T, K> : retire certaines clés
type UtilisateurSansMdp = Omit<Utilisateur, "motDePasse">;

// Record<K, V> : dictionnaire typé clé -> valeur
type RolesParId = Record<number, "admin" | "membre">;
const roles: RolesParId = { 1: "admin", 2: "membre" };

// Exclude<T, U> / Extract<T, U> : filtrent une union
type Reponse = "ok" | "erreur" | "chargement" | "annule";
type ReponsesFinales = Exclude<Reponse, "chargement">; // "ok" | "erreur" | "annule"
type ReponsesEchec = Extract<Reponse, "erreur" | "annule">;

// NonNullable<T> : retire null et undefined
type Peutetre = string | null | undefined;
type Certain = NonNullable<Peutetre>; // string

// ReturnType<T> / Parameters<T> : extraient les types depuis une fonction existante
function creerUtilisateur(nom: string, email: string) {
  return { id: Date.now(), nom, email };
}
type NouvelUtilisateur = ReturnType<typeof creerUtilisateur>;
type ParametresCreation = Parameters<typeof creerUtilisateur>; // [string, string]

// Awaited<T> : "déballe" le type d'une Promise (y compris imbriquée)
async function chargerUtilisateur(): Promise<Utilisateur> {
  return {} as Utilisateur;
}
type UtilisateurCharge = Awaited<ReturnType<typeof chargerUtilisateur>>;

// InstanceType<T> : type de l'instance produite par un constructeur
class Service {
  demarrer() {
    return true;
  }
}
type InstanceService = InstanceType<typeof Service>;
UtilityRôle
Partial<T> / Required<T>rend optionnel / obligatoire
Pick<T,K> / Omit<T,K>garde / retire des clés
Record<K,V>dictionnaire typé
ReturnType<T> / Parameters<T>extraient depuis une fonction

Résumé

  • Ces types dérivent un nouveau type depuis un type existant sans dupliquer sa définition.
  • typeof fonction + ReturnType/Parameters évite de désynchroniser signature et types.
  • Combinables : Partial<Omit<Utilisateur, "id">> reste parfaitement valide.

Exercices pratiques

1 disponible
1

Mission : réparer les variantes désynchronisées de Utilisateur

Objectif : Remplacer deux interfaces dupliquées à la main par des utility types dérivés, puis diagnostiquer les limites restantes de cette approche.

Contexte

Une équipe maintient UtilisateurSansMotDePasse et UtilisateurMiseAJour, deux interfaces recopiées à la main depuis Utilisateur. Quand un champ telephone est ajouté à Utilisateur, les deux variantes ne sont pas mises à jour, et l'API publique continue de ne pas exposer ce champ alors qu'elle le devrait. Ta mission : remplacer ces duplications par des types dérivés qui se synchronisent automatiquement.

Résoudre l’exercice →