Leçon 7/10 10 min

Les types utilitaires

Partial, Pick, Omit, Record : transformer les types sans les copier. Pourquoi une copie de type diverge en silence.

La tentation du copier-coller de types

Imaginons une interface Article avec quatre champs : id, titre, contenu, brouillon. Tu dois écrire une fonction de mise à jour partielle, le genre qui n'accepte que les champs qu'on veut modifier. La tentation est immédiate : copier l'interface, coller, ajouter des ? partout.

interface Article {
  id: number;
  titre: string;
  contenu: string;
  brouillon: boolean;
}

// La tentation : copier-coller et tout rendre optionnel
interface ArticlePartiel {
  id?: number;
  titre?: string;
  contenu?: string;
  brouillon?: boolean;
}

Deux semaines après, on ajoute un champ auteur à Article. La copie ne le sait pas. Elle diverge en silence. La fonction de mise à jour accepte { auteur: 'Bob' } en JavaScript mais TS ne voit pas le problème dans ArticlePartiel : le champ n'y est pas. Le bug s'installe doucement, porté par une copie qu'on avait oubliée.

Piège classique : une interface copiée-modifiée est une bombe à retardement. Dès que la source évolue, la copie diverge. Le compilateur ne peut pas relier les deux : il ne sait pas que tu voulais une variante de l'original.

Prédis avant de lire

changements est typé Partial<Article>. Lesquels de ces objets passent : { titre: 'X' }, {}, { titrre: 'X' }, { id: 2, brouillon: false } ?

Voir la réponse

Tous passent sauf { titrre: 'X' }. Partial<Article> rend chaque clé d'Article optionnelle : l'objet vide {} est valide, un sous-ensemble comme { titre: 'X' } est valide, plusieurs champs comme { id: 2, brouillon: false } aussi. Mais la typo titrre est refusée avec suggestion, parce que Partial rend les clés optionnelles, il n'en invente pas de nouvelles.

Transformer au lieu de réécrire

Les types utilitaires sont des génériques (leçon 6) fournis par TypeScript qui fabriquent un type à partir d'un autre. La source de vérité reste une seule interface : toutes les variantes en dérivent et suivent ses évolutions automatiquement.

interface Article {
  id: number;
  titre: string;
  contenu: string;
  brouillon: boolean;
}

// Tous les champs deviennent optionnels : parfait pour une mise à jour partielle
type ArticlePartiel = Partial<Article>;

// Garder uniquement id et titre : idéal pour un résumé ou une liste
type ArticleResume = Pick<Article, 'id' | 'titre'>;

// Tout sauf contenu : par exemple pour un aperçu sans le corps
type ArticleSansCorps = Omit<Article, 'contenu'>;

// Dictionnaire : clé string, valeur number (compteur de vues par slug)
type VuesParSlug = Record<string, number>;

// Tous les champs en lecture seule : immuable après création
type ArticleFige = Readonly<Article>;

Un exemple concret par utilitaire, une ligne chacun. Ajoute auteur: string à Article : ArticlePartiel, ArticleResume, ArticleSansCorps et ArticleFige le voient immédiatement. Zéro mise à jour manuelle, zéro divergence.

Règle d'or : une seule source de vérité. Définis l'interface complète une fois, dérive toutes les variantes depuis elle. Quand la source évolue, les dérivés suivent. Si tu te retrouves à copier-coller une interface pour la modifier, cherche d'abord l'utilitaire qui fait ça.

L'interface Article est la source de vérité centrale. Trois flèches en partent vers trois types dérivés : Partial<Article> avec tous les champs optionnels, Pick avec uniquement id et titre, Omit avec le champ contenu barré. interface Article id: number titre: string contenu: string brouillon: boolean Partial<Article> id?: number titre?: string contenu?: string brouillon?: boolean Pick<Article, 'id'|'titre'> id: number titre: string Omit<Article, 'contenu'> id: number titre: string contenu
Une seule source de vérité, trois dérivés toujours synchronisés.

À toi : Partial piège la faute de frappe

Partial<Article> accepte un sous-ensemble des champs, mais pas un champ qui n'existe pas : la faute de frappe dans une mise à jour partielle, classique en JavaScript, est morte ici aussi.

🧐 Labo TypeScript · le compilateur juge ton code (mode strict)
Bloqué sur la correction ? Voir le fix

Corrige titrre en titre. Le message d'erreur le suggère lui-même : Object literal may only specify known properties… 'titrre' does not exist on type 'Partial<Article>'. Did you mean 'titre'? Partial rend les clés optionnelles, il n'invente pas de nouvelles clés. Revérifie : ✓ 0 erreur, puis Exécuter : « TypeScript ».

Le réflexe : cherche l'utilitaire avant de réécrire

À chaque fois que tu t'apprêtes à écrire un type « presque pareil » à un autre, pause. La question à se poser : est-ce que je veux rendre des champs optionnels, garder certains champs, en enlever quelques-uns, ou décrire un dictionnaire ? Si oui, il y a un utilitaire pour ça.

Ces utilitaires ne sont pas magiques. Ce sont des types génériques que tu pourrais écrire toi-même. Partial<T> est un mapped type : { [K in keyof T]?: T[K] }. Comprendre ça, c'est voir que le système de types de TS est composable : les utilitaires de la bibliothèque standard suivent exactement les mêmes règles que les types que tu définis.

Il en existe d'autres : Required (l'inverse de Partial), ReturnType<T> (le type de retour d'une fonction), Parameters<T>, NonNullable<T>... Le Handbook TypeScript (Utility Types) les liste tous avec des exemples.

Composer les utilitaires : extends ou & ?

Les utilitaires se combinent. Un formulaire de création d'article partiellement prérempli ? C'est Partial<Article> plus un champ obligatoire en plus — deux types à fusionner.

// Fusionner avec &
type BrouillonForcé = Partial<Article> & { brouillon: true }

// Même résultat avec extends
interface BrouillonForcé extends Partial<Article> {
  brouillon: true
}

Dans ce cas, les deux sont équivalents. La différence apparaît sur un conflit de propriété :

interface A { id: string }
interface B { id: number }

interface C extends A, B {}    // ❌ Erreur à la déclaration
                               //    Types of property 'id' are incompatible

type C = A & B                 // ✅ Aucune erreur à la déclaration…
const c: C = { id: ??? }       // ❌ …mais id vaut string & number = never
                               //    Aucune valeur possible

Le piège de & : quand deux types ont la même clé avec des types incompatibles, & ne plante pas à la déclaration — il rend la propriété never. L'erreur arrive plus tard, quand tu essaies de l'utiliser. extends échoue tôt et fort : préfère-le pour l'héritage objet à objet.

Règle pratique : utilise & pour mixer des formes hétérogènes ou des unions ((Succès | Erreur) & { date: Date }) — extends ne sait pas faire ça. Pour l'héritage objet à objet sans conflit, extends est plus lisible et échoue plus tôt.

The copy-paste type temptation

Imagine an Article interface with four fields: id, title, content, draft. You need to write a partial update function, the kind that only accepts the fields you want to change. The temptation is immediate: copy the interface, paste it, add ? everywhere.

interface Article {
  id: number;
  title: string;
  content: string;
  draft: boolean;
}

// The temptation: copy-paste and make everything optional
interface PartialArticle {
  id?: number;
  title?: string;
  content?: string;
  draft?: boolean;
}

Two weeks later, an author field gets added to Article. The copy doesn't know. It diverges silently. The update function accepts { author: 'Bob' } in JavaScript but TS doesn't flag it in PartialArticle: the field isn't there. The bug quietly settles in, carried by a copy everyone forgot about.

Classic trap: a copied-then-modified interface is a time bomb. As soon as the source evolves, the copy diverges. The compiler can't connect the two: it doesn't know you wanted a variant of the original.

Predict before reading on

changes is typed Partial<Article>. Which of these objects are accepted: { title: 'X' }, {}, { titlle: 'X' }, { id: 2, draft: false }?

Show the answer

All pass except { titlle: 'X' }. Partial<Article> makes every key of Article optional: the empty object {} is valid, a subset like { title: 'X' } is valid, multiple fields like { id: 2, draft: false } too. But the typo titlle is rejected with a suggestion, because Partial makes keys optional — it doesn't invent new ones.

Transform instead of rewrite

Utility types are generics (lesson 6) provided by TypeScript that produce a type from another. The single source of truth remains one interface: all variants derive from it and follow its changes automatically.

interface Article {
  id: number;
  title: string;
  content: string;
  draft: boolean;
}

// All fields become optional — perfect for a partial update
type PartialArticle = Partial<Article>;

// Keep only id and title — ideal for a summary or list
type ArticleSummary = Pick<Article, 'id' | 'title'>;

// Everything except content — e.g. a preview without the body
type ArticleWithoutContent = Omit<Article, 'content'>;

// Dictionary: string key, number value — view count per slug
type ViewsBySlug = Record<string, number>;

// All fields read-only — immutable after creation
type FrozenArticle = Readonly<Article>;

One concrete example per utility, one line each. Add author: string to Article: PartialArticle, ArticleSummary, ArticleWithoutContent, and FrozenArticle all see it immediately. Zero manual updates, zero drift.

Golden rule: one source of truth. Define the full interface once, derive all variants from it. When the source evolves, the derived types follow. If you catch yourself copying and modifying an interface, look for the utility that does it.

The Article interface is the central source of truth. Three arrows lead to three derived types: Partial with all fields optional, Pick with only id and title, Omit with the content field crossed out. interface Article id: number title: string content: string draft: boolean Partial<Article> id?: number title?: string content?: string draft?: boolean Pick<Article, 'id'|'title'> id: number title: string Omit<Article, 'content'> id: number title: string content
One source of truth, three derived types always in sync.

Your turn: Partial catches the typo

Partial<Article> accepts a subset of fields, but not a field that doesn't exist: the typo in a partial update — a classic JS pitfall — is caught here too.

🧐 TypeScript lab · the compiler judges your code (strict mode)
Stuck on the fix? Show it

Fix titlle to title. The error message suggests it: Object literal may only specify known properties… 'titlle' does not exist on type 'Partial<Article>'. Did you mean 'title'? Partial makes keys optional — it doesn't invent new ones. Re-check: ✓ 0 errors, then Run: "TypeScript".

The reflex: look for the utility before rewriting

Every time you're about to write a type that's "almost the same" as another, pause. The question to ask: do I want to make fields optional, keep certain fields, remove some, or describe a dictionary? If yes, there's a utility for that.

These utilities aren't magic. They're generic types you could write yourself. Partial<T> is a mapped type: { [K in keyof T]?: T[K] }. Understanding this shows that TS's type system is composable: the standard library utilities follow exactly the same rules as the types you define.

There are more: Required (the opposite of Partial), ReturnType<T> (the return type of a function), Parameters<T>, NonNullable<T>... The TypeScript Handbook (Utility Types) lists them all with examples.

Composing utilities: extends or &?

Utilities can be combined. A partially pre-filled article creation form? That's Partial<Article> plus one required extra field — two types to merge.

// Merge with &
type ForcedDraft = Partial<Article> & { draft: true }

// Same result with extends
interface ForcedDraft extends Partial<Article> {
  draft: true
}

Here both are equivalent. The difference shows up on a property conflict:

interface A { id: string }
interface B { id: number }

interface C extends A, B {}    // ❌ Error at declaration
                               //    Types of property 'id' are incompatible

type C = A & B                 // ✅ No error at declaration…
const c: C = { id: ??? }       // ❌ …but id is string & number = never
                               //    No value can satisfy it

The & trap: when two types share a key with incompatible types, & doesn't fail at declaration — it makes the property never. The error shows up later, when you try to use it. extends fails early and loudly: prefer it for classic object-to-object inheritance.

Practical rule: use & to mix heterogeneous shapes or unions ((Success | Error) & { date: Date }) — extends can't handle that. For object-to-object inheritance without conflict, extends is more readable and fails sooner.

🎯 Pratique

S'entraîner (clique pour ouvrir) :

💬 Ré-explique sans regarder
Ré-explique sans regarder

Explique le problème du copier-coller de types et comment Partial, Pick et Omit le règlent.

Une bonne explication dit : la copie diverge dès qu'Article évolue (champ ajouté non reporté) ; les utilitaires dérivent depuis une source unique et suivent ses évolutions automatiquement ; Partial rend chaque clé optionnelle mais refuse les clés inconnues (la typo est stoppée) ; Pick et Omit filtrent les champs sans réécrire l'interface.
🧠 Rappel libre
Rappel libre

De mémoire : Partial, Pick, Omit, Record : qui fait quoi ?

Partial : rend tous les champs optionnels. Pick : ne garde que les clés listées. Omit : enlève les clés listées. Record : dictionnaire clé → valeur (ex. Record<string, number> = objet dont les clés sont des string et les valeurs des number).
⚖️ Juge le code de l'IA
Accepter ou rejeter le code de l'IA

Tu demandes à l'IA une fonction de mise à jour partielle d'un article. Elle génère changements: any en expliquant : « Comme ça, tous les champs partiels passent. » Tu acceptes, ou tu rejettes ?

À rejeter. any donne bien la souplesse voulue, mais au prix de toute sécurité : { titrre: 'X' } passe, { champInventé: 42 } passe, n'importe quelle clé passe. C'est exactement le problème JS que TypeScript venait résoudre. Partial<Article> donne la même souplesse (tous les champs optionnels) en gardant le contrat : seules les clés d'Article sont acceptées. C'est le cas d'école où l'utilitaire bat any.
Que fait Partial<Article> ?
Tu veux un type avec uniquement id et titre d'Article. Lequel utiliser ?
Pourquoi dériver (Partial/Pick/Omit) plutôt que copier-coller l'interface en la modifiant ?
Record<string, number> décrit quoi, et de quelle leçon ces utilitaires sont-ils l'application directe ?
Prochaine étape

Tu sais maintenant fabriquer des types à la demande. Mais une question reste en suspens : que devient tout ça quand le code s'exécute vraiment ? À la leçon 8, tu découvres que les types sont effacés à l'exécution, ce que « as » promet (et peut trahir), et pourquoi une API peut te mentir même avec TypeScript.

Leçon 8 : Compilation et runtime →
Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement