Types utilitaires#

Hide code cell source

import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

Les types utilitaires sont une collection de types génériques prédéfinis par TypeScript, disponibles sans aucun import. Ils permettent de transformer des types existants : rendre des propriétés optionnelles, en sélectionner un sous-ensemble, extraire le type de retour d’une fonction, etc. Construits sur les mécanismes de génériques, de keyof, des types conditionnels et des types mappés, ils constituent la boîte à outils quotidienne de tout développeur TypeScript sérieux.

Transformation de propriétés#

Partial<T>#

Définition 18 (Partial)

Partial<T> construit un nouveau type dans lequel toutes les propriétés de T deviennent optionnelles. C’est l’utilitaire idéal pour les mises à jour partielles d’objets, les formulaires ou les paramètres de configuration avec valeurs par défaut.

interface Utilisateur {
  id: number;
  nom: string;
  email: string;
  role: "admin" | "utilisateur";
}

type MiseAJourUtilisateur = Partial<Utilisateur>;
// Équivalent à :
// { id?: number; nom?: string; email?: string; role?: "admin" | "utilisateur" }

function mettreAJour(id: number, modifications: Partial<Utilisateur>): void {
  // On peut ne passer que les champs à modifier
  console.log(`Mise à jour de l'utilisateur ${id}`, modifications);
}

mettreAJour(1, { nom: "Alice Dupont" });       // ✓ seul nom est modifié
mettreAJour(2, { email: "bob@exemple.fr" }); // ✓ seul email est modifié

Required<T>#

Required<T> est l’inverse de Partial<T> : il rend toutes les propriétés obligatoires, même celles marquées optionnelles dans le type original.

interface OptionsServeur {
  hôte?: string;
  port?: number;
  ssl?: boolean;
}

type ConfigurationComplète = Required<OptionsServeur>;
// { hôte: string; port: number; ssl: boolean }

function démarrer(config: ConfigurationComplète): void {
  console.log(`Démarrage sur ${config.hôte}:${config.port}`);
}

Readonly<T>#

Définition 19 (Readonly)

Readonly<T> construit un type dont toutes les propriétés sont marquées readonly : elles ne peuvent plus être réassignées après création de l’objet. Utile pour exprimer l’immuabilité au niveau du type, notamment pour les constantes de configuration ou les données partagées.

interface Point {
  x: number;
  y: number;
}

const origine: Readonly<Point> = { x: 0, y: 0 };
// origine.x = 1; // Erreur : Cannot assign to 'x' because it is a read-only property

// Très courant avec les tableaux
function somme(valeurs: Readonly<number[]>): number {
  return valeurs.reduce((acc, v) => acc + v, 0);
  // valeurs.push(4); // Erreur — le tableau est en lecture seule
}

Remarque 14

Readonly<T> est superficiel (shallow) : il rend les propriétés directes de T en lecture seule, mais les objets imbriqués restent mutables. Pour une immutabilité profonde, il faut construire DeepReadonly<T> à la main — ce que nous verrons en section 6. De même, ReadonlyArray<T> (ou readonly T[]) interdit push, pop et les assignations indexées, mais pas la mutation des objets contenus dans le tableau.

Sélection et exclusion#

Pick<T, K>#

Définition 20 (Pick)

Pick<T, K> construit un nouveau type en sélectionnant uniquement les propriétés dont les clés appartiennent à l’union K. K doit être une union de clés valides de T (contrainte : K extends keyof T). C’est l’équivalent d’une projection en algèbre relationnelle.

interface Article {
  id: number;
  titre: string;
  contenu: string;
  datePublication: Date;
  auteur: string;
  vues: number;
}

// Pour un résumé (liste d'articles), on ne veut pas le contenu complet
type RésuméArticle = Pick<Article, "id" | "titre" | "auteur" | "datePublication">;

function listerArticles(): RésuméArticle[] {
  return [
    { id: 1, titre: "TypeScript avancé", auteur: "Alice", datePublication: new Date() },
  ];
}

Omit<T, K>#

Omit<T, K> est le complémentaire de Pick : il construit un type en excluant les propriétés dont les clés appartiennent à K.

// Pour créer un article, on n'a pas encore d'id ni de compteur de vues
type NouvelArticle = Omit<Article, "id" | "vues">;

function créerArticle(données: NouvelArticle): Article {
  return { ...données, id: Math.random(), vues: 0 };
}

Record<K, V>#

Définition 21 (Record)

Record<K, V> construit un type objet dont les clés sont du type K (qui doit être assignable à string | number | symbol) et les valeurs du type V. C’est un moyen expressif de typer des dictionnaires ou des tables de correspondance.

type CodePays = "FR" | "DE" | "ES" | "IT";

interface InfoPays {
  nom: string;
  population: number;
  capitale: string;
}

const pays: Record<CodePays, InfoPays> = {
  FR: { nom: "France",    population: 68_000_000, capitale: "Paris" },
  DE: { nom: "Allemagne", population: 84_000_000, capitale: "Berlin" },
  ES: { nom: "Espagne",   population: 47_000_000, capitale: "Madrid" },
  IT: { nom: "Italie",    population: 60_000_000, capitale: "Rome" },
};

// Record est pratique pour créer des index ou des caches
type Cache<T> = Record<string, T | undefined>;

const cacheUtilisateurs: Cache<Utilisateur> = {};

Manipulation d’unions#

Exclude<T, U>#

Définition 22 (Exclude)

Exclude<T, U> produit une union en retirant de T tous les membres assignables à U. Il opère sur des unions, pas sur des objets.

type Primitif = string | number | boolean | null | undefined;

type PrimitifNonNull = Exclude<Primitif, null | undefined>;
// string | number | boolean

type SansBoolean = Exclude<string | number | boolean, boolean>;
// string | number

type ÉvénementSouris = "click" | "mousedown" | "mouseup" | "mousemove" | "keydown";
type ÉvénementClavier = "keydown" | "keyup" | "keypress";

type ÉvénementsSourisSeulement = Exclude<ÉvénementSouris, ÉvénementClavier>;
// "click" | "mousedown" | "mouseup" | "mousemove"

Extract<T, U>#

Extract<T, U> est le complément d”Exclude : il conserve uniquement les membres de T qui sont assignables à U.

type Commun = Extract<string | number | boolean, number | boolean>;
// number | boolean

type ÉvénementsCommuns = Extract<ÉvénementSouris, ÉvénementClavier>;
// "keydown"

NonNullable<T>#

Définition 23 (NonNullable)

NonNullable<T> retire null et undefined d’un type union. C’est une application spécialisée d”Exclude : NonNullable<T> est équivalent à Exclude<T, null | undefined>. Il est particulièrement utile après un contrôle de nullité pour affiner le type.

type ValeurOptionnelle = string | number | null | undefined;
type ValeurCertaine = NonNullable<ValeurOptionnelle>;
// string | number

function traiter<T>(valeur: T): NonNullable<T> {
  if (valeur == null) throw new Error("Valeur nulle inattendue.");
  return valeur as NonNullable<T>;
}

Introspection de fonctions#

TypeScript fournit des utilitaires pour extraire des informations de type à partir des signatures de fonctions et de constructeurs.

Parameters<T> et ReturnType<T>#

Définition 24 (Parameters et ReturnType)

  • Parameters<T> : extrait les types des paramètres d’une fonction T sous forme de tuple.

  • ReturnType<T> : extrait le type de retour d’une fonction T.

Ces deux utilitaires sont implémentés avec infer dans des types conditionnels (voir chapitre 8).

function créerSession(
  utilisateurId: number,
  durée: number,
  options?: { renouveler: boolean }
): { jeton: string; expiration: Date } {
  return { jeton: crypto.randomUUID(), expiration: new Date() };
}

type ParamètresCréerSession = Parameters<typeof créerSession>;
// [utilisateurId: number, durée: number, options?: { renouveler: boolean }]

type SessionCrée = ReturnType<typeof créerSession>;
// { jeton: string; expiration: Date }

// Cas d'usage : wrapper qui suit les appels
function avecJournal<T extends (...args: any[]) => any>(
  fn: T
): (...args: Parameters<T>) => ReturnType<T> {
  return (...args) => {
    console.log(`Appel avec`, args);
    const résultat = fn(...args);
    console.log(`Résultat :`, résultat);
    return résultat;
  };
}

ConstructorParameters<T> et InstanceType<T>#

class Connexion {
  constructor(
    readonly hôte: string,
    readonly port: number,
    readonly ssl: boolean
  ) {}

  connecter(): Promise<void> {
    return Promise.resolve();
  }
}

type ParamètresConnexion = ConstructorParameters<typeof Connexion>;
// [hôte: string, port: number, ssl: boolean]

type InstanceConnexion = InstanceType<typeof Connexion>;
// Connexion

// Fabrique générique
function fabriquer<T extends new (...args: any[]) => any>(
  Constructeur: T,
  ...args: ConstructorParameters<T>
): InstanceType<T> {
  return new Constructeur(...args);
}

const conn = fabriquer(Connexion, "localhost", 5432, false);
// conn : Connexion — type précis inféré

Remarque 15

typeof Connexion et Connexion désignent des choses différentes : Connexion est le type des instances de la classe (équivalent à InstanceType<typeof Connexion>), tandis que typeof Connexion est le type de la classe elle-même (le constructeur). ConstructorParameters et InstanceType attendent typeof Connexion, pas Connexion.

Types de chaînes#

TypeScript fournit quatre utilitaires pour manipuler les types de chaînes littérales (pas les valeurs, mais les types) :

type Salutation = "bonjour" | "bonsoir";

type EnMajuscules  = Uppercase<Salutation>;    // "BONJOUR" | "BONSOIR"
type EnMinuscules  = Lowercase<"ALPHA" | "BETA">; // "alpha" | "beta"
type Capitalisé    = Capitalize<Salutation>;   // "Bonjour" | "Bonsoir"
type Décapitalisé  = Uncapitalize<"Bonjour" | "Bonsoir">; // "bonjour" | "bonsoir"

Ces utilitaires sont particulièrement utiles pour dériver des noms de propriétés de façon programmatique :

type GéteursPour<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Config {
  hôte: string;
  port: number;
}

type GettersConfig = GéteursPour<Config>;
// { getHôte: () => string; getPort: () => number }

Exemple 6 (Construction d’une API fluide)

type Événements = "click" | "focus" | "blur";

// Générer les noms de gestionnaires : onClick, onFocus, onBlur
type Gestionnaires = {
  [K in Événements as `on${Capitalize<K>}`]: (event: Event) => void;
};
// { onClick: (event: Event) => void;
//   onFocus: (event: Event) => void;
//   onBlur:  (event: Event) => void }

Combinaisons pratiques#

L’intérêt des types utilitaires se révèle pleinement lorsqu’on les compose ou qu’on les étend pour répondre à des besoins spécifiques.

DeepPartial<T>#

Partial<T> n’est pas récursif : les propriétés de premier niveau deviennent optionnelles, mais les objets imbriqués restent inchangés. On peut construire une version récursive :

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

interface Adresse {
  rue: string;
  ville: string;
  pays: string;
}

interface Profil {
  nom: string;
  âge: number;
  adresse: Adresse;
}

type ProfilPartiel = DeepPartial<Profil>;
// { nom?: string; âge?: number; adresse?: { rue?: string; ville?: string; pays?: string } }

function mettreAJourProfil(id: string, modifications: DeepPartial<Profil>): void {
  // On peut ne mettre à jour que la ville sans fournir rue et pays
}
mettreAJourProfil("42", { adresse: { ville: "Lyon" } }); // ✓

Mutable<T>#

Readonly<T> n’a pas d’équivalent officiel pour retirer readonly. On peut le construire en utilisant le modificateur -readonly des types mappés :

type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

interface ConfigImmuable {
  readonly hôte: string;
  readonly port: number;
}

type ConfigModifiable = Mutable<ConfigImmuable>;
// { hôte: string; port: number } — readonly retiré

// De même, on peut retirer l'optionnalité :
type Obligatoire<T> = {
  [K in keyof T]-?: T[K];
};

Remarque 16

Les modificateurs de type mappé -readonly et -? permettent respectivement de retirer readonly et l’optionnalité (?) sur chaque propriété. Leur pendant positif (readonly et ?) est celui qu’utilisent Readonly<T> et Partial<T>. Cette symétrie rend le système de types très expressif pour transformer des types en profondeur.

Composition#

On peut enchaîner les types utilitaires pour exprimer des transformations complexes de façon lisible :

// Un formulaire de mise à jour : toutes les propriétés sauf id sont optionnelles et non-null
type FormulaireEdition<T extends { id: unknown }> = Partial<Omit<T, "id">> & Pick<T, "id">;

// Exemple : éditer un Utilisateur
type FormulaireUtilisateur = FormulaireEdition<Utilisateur>;
// { id: number } & { nom?: string; email?: string; role?: "admin" | "utilisateur" }

// Lire en lecture seule uniquement certaines propriétés
type ArticleSûr = Readonly<Pick<Article, "id" | "titre">> & Omit<Article, "id" | "titre">;

Visualisation : grille des types utilitaires#

Hide code cell source

import matplotlib.pyplot as plt
import matplotlib.patches as patches
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

fig, ax = plt.subplots(figsize=(15, 10))
ax.set_xlim(0, 15)
ax.set_ylim(0, 10)
ax.axis("off")
ax.set_title("Types utilitaires TypeScript — Entrée → Transformation → Sortie",
             fontsize=14, fontweight="bold", pad=14)

palette = sns.color_palette("muted")

# Définition des lignes du tableau
rangees = [
    # (catégorie, utilitaire, entrée, sortie)
    ("Propriétés", "Partial<T>",              "{ id: number; nom: string }",          "{ id?: number; nom?: string }"),
    ("Propriétés", "Required<T>",             "{ id?: number; nom?: string }",         "{ id: number; nom: string }"),
    ("Propriétés", "Readonly<T>",             "{ x: number; y: number }",              "{ readonly x: number; readonly y: number }"),
    ("Sélection",  "Pick<T, 'id'|'nom'>",     "{ id: number; nom: string; email: string }", "{ id: number; nom: string }"),
    ("Sélection",  "Omit<T, 'email'>",        "{ id: number; nom: string; email: string }", "{ id: number; nom: string }"),
    ("Sélection",  "Record<'fr'|'de', Info>", "'fr' | 'de',  Info",                   "{ fr: Info; de: Info }"),
    ("Unions",     "Exclude<T, null>",         "string | number | null",               "string | number"),
    ("Unions",     "Extract<T, number>",       "string | number | boolean",            "number"),
    ("Unions",     "NonNullable<T>",           "string | null | undefined",            "string"),
    ("Fonctions",  "Parameters<T>",            "(a: number, b: string) => void",       "[number, string]"),
    ("Fonctions",  "ReturnType<T>",            "() => { jeton: string }",              "{ jeton: string }"),
    ("Chaînes",    "Capitalize<T>",            "'bonjour' | 'bonsoir'",                "'Bonjour' | 'Bonsoir'"),
    ("Chaînes",    "Uppercase<T>",             "'alpha' | 'beta'",                     "'ALPHA' | 'BETA'"),
]

# Couleurs par catégorie
couleur_cat = {
    "Propriétés": (palette[0], "#1a5276"),
    "Sélection":  (palette[1], "#784212"),
    "Unions":     (palette[2], "#145a32"),
    "Fonctions":  (palette[3], "#4a235a"),
    "Chaînes":    (palette[4], "#1b2631"),
}

n = len(rangees)
hauteur_rangee = 9.0 / (n + 1)
y_entete = 9.3

# En-têtes de colonnes
col_positions = [0.1, 2.9, 6.1, 10.8]
col_largeurs  = [2.7, 3.0,  4.5,  4.0]
entetes = ["Catégorie", "Utilitaire", "Type d'entrée", "Type de sortie"]
for i, (x, lbl) in enumerate(zip(col_positions, entetes)):
    fond = patches.FancyBboxPatch(
        (x, y_entete - 0.38), col_largeurs[i], 0.42,
        boxstyle="round,pad=0.05",
        facecolor="#2c3e50", edgecolor="none"
    )
    ax.add_patch(fond)
    ax.text(x + col_largeurs[i] / 2, y_entete - 0.16,
            lbl, ha="center", va="center",
            fontsize=9, fontweight="bold", color="white")

# Lignes de données
dernier_cat = None
for idx, (cat, util, entree, sortie) in enumerate(rangees):
    y = y_entete - 0.38 - (idx + 1) * hauteur_rangee
    couleur_fond, couleur_bord = couleur_cat[cat]

    # Fond de ligne (alterné)
    bg_alpha = 0.12 if idx % 2 == 0 else 0.06
    fond_ligne = patches.FancyBboxPatch(
        (0.05, y - 0.03), 14.9, hauteur_rangee - 0.06,
        boxstyle="round,pad=0.04",
        facecolor=couleur_fond, edgecolor="none", alpha=bg_alpha
    )
    ax.add_patch(fond_ligne)

    # Cellule catégorie (uniquement à la première occurrence)
    if cat != dernier_cat:
        ax.text(col_positions[0] + col_largeurs[0] / 2, y + hauteur_rangee / 2 - 0.04,
                cat, ha="center", va="center",
                fontsize=8.5, fontweight="bold", color=couleur_bord,
                bbox=dict(boxstyle="round,pad=0.2", facecolor=couleur_fond,
                          edgecolor=couleur_bord, alpha=0.5))
        dernier_cat = cat

    # Utilitaire (monospace)
    ax.text(col_positions[1] + 0.08, y + hauteur_rangee / 2 - 0.04,
            util, ha="left", va="center",
            fontsize=8, fontfamily="monospace", color="#1c1c1c")

    # Entrée
    ax.text(col_positions[2] + 0.08, y + hauteur_rangee / 2 - 0.04,
            entree, ha="left", va="center",
            fontsize=7.2, fontfamily="monospace", color="#555555")

    # Flèche →
    ax.text(10.55, y + hauteur_rangee / 2 - 0.04,
            "→", ha="center", va="center",
            fontsize=13, color=couleur_bord, fontweight="bold")

    # Sortie
    ax.text(col_positions[3] + 0.08, y + hauteur_rangee / 2 - 0.04,
            sortie, ha="left", va="center",
            fontsize=7.2, fontfamily="monospace", color="#145a32",
            fontweight="bold")

plt.tight_layout()
plt.show()
_images/5c90f821477fb5cd83e3de6e621075221b6f0c737dca61441d3455a8e302a208.png

Résumé#

Ce chapitre a passé en revue l’ensemble des types utilitaires fournis par TypeScript :

  • Transformation de propriétés : Partial<T> rend toutes les propriétés optionnelles, Required<T> les rend obligatoires, Readonly<T> les rend non-réassignables. Ces transformations sont superficielles (premier niveau seulement).

  • Sélection et exclusion : Pick<T, K> extrait un sous-ensemble de propriétés, Omit<T, K> en exclut certaines, Record<K, V> construit un dictionnaire typé.

  • Manipulation d’unions : Exclude<T, U> retire des membres d’une union, Extract<T, U> n’en conserve que certains, NonNullable<T> élimine null et undefined.

  • Introspection de fonctions : Parameters<T> et ReturnType<T> extraient les types de paramètres et de retour ; ConstructorParameters<T> et InstanceType<T> font de même pour les classes.

  • Types de chaînes : Uppercase, Lowercase, Capitalize, Uncapitalize transforment des types de chaînes littérales, idéaux pour générer des noms de propriétés programmatiquement.

  • Compositions : DeepPartial<T>, Mutable<T> et d’autres types construits à la main démontrent la puissance de la composition. Les modificateurs -readonly et -? dans les types mappés permettent l’inverse de Readonly et Partial.

Dans le chapitre suivant, nous explorerons le mécanisme sous-jacent qui rend possible nombre de ces types utilitaires : les types conditionnels, avec leur syntaxe T extends U ? X : Y, la distribution sur les unions, et le mot-clé infer pour l’extraction de sous-types.