TypeScript le système de types - 10 - Utility types : Partial, Pick, Omit, Record et les autres

Les utility types intégrés de TypeScript. Comment les utiliser, comment ils fonctionnent sous le capot, et quand choisir lequel.

10 - Utility types : Partial, Pick, Omit, Record et les autres

Ce que tu vas apprendre

  • Les utility types les plus utilises et quand les appliquer
  • Comment ils fonctionnent sous le capot (mapped types, conditional types)
  • Les combinaisons courantes pour modéliser des cas réels
  • Les utility types moins connus qui meritent ton attention

Prerequisites

Avoir lu les articles sur les generics et la null safety.


Pourquoi les utility types existent

En travaillant sur une API REST, tu as souvent besoin de variantes d'un meme type. L'utilisateur complet pour la lecture, une version partielle pour la mise à jour, une version sans l'id pour la création, une version avec juste le nom et l'email pour l'affichage.

Sans utility types, tu dupliques :

typescriptinterface User {
  id: string
  name: string
  email: string
  role: "admin" | "user"
  createdAt: Date
}

// ❌ Duplication
interface CreateUser {
  name: string
  email: string
  role: "admin" | "user"
}

interface UpdateUser {
  name?: string
  email?: string
  role?: "admin" | "user"
}

interface UserSummary {
  name: string
  email: string
}

Quatre types a maintenir. Si tu ajoutes un champ phone a User, tu dois penser a le rajouter dans CreateUser aussi. Les utility types resolvent ca.

typescripttype CreateUser = Omit<User, "id" | "createdAt">
type UpdateUser = Partial<CreateUser>
type UserSummary = Pick<User, "name" | "email">

Un seul type source. Les variantes en derivent.

Partial

Rend toutes les propriétés optionnelles :

typescripttype UpdateUser = Partial<User>
// {
//   id?: string
//   name?: string
//   email?: string
//   role?: "admin" | "user"
//   createdAt?: Date
// }

Le cas d'usage principal : les updates partielles. Tu envoies uniquement les champs a modifier.

typescriptasync function updateUser(id: string, data: Partial<User>): Promise<User> {
  return prisma.user.update({ where: { id }, data })
}

await updateUser("123", { name: "Nicolas" }) // seul name change

Sous le capot, Partial est un mapped type :

typescripttype Partial<T> = {
  [K in keyof T]?: T[K]
}

Il itéré sur chaque clé de T et ajoute ?. Les mapped types sont couverts dans la sous-serie types avances.

Required

L'inverse de Partial. Rend toutes les propriétés obligatoires :

typescriptinterface Config {
  port?: number
  host?: string
  debug?: boolean
}

type StrictConfig = Required<Config>
// {
//   port: number
//   host: string
//   debug: boolean
// }

Utile pour les configurations ou tu veux garantir que tous les champs ont ete remplis apres le merge avec les défauts.

typescriptconst defaults: Required<Config> = {
  port: 3000,
  host: "localhost",
  debug: false
}

function loadConfig(overrides: Config): Required<Config> {
  return { ...defaults, ...overrides }
}

Pick

Extrait un sous-ensemble de propriétés :

typescripttype UserSummary = Pick<User, "name" | "email">
// {
//   name: string
//   email: string
// }

Je l'utilise pour les DTOs (Data Transfer Objects) dans les APIs. Le front n'a pas besoin de toutes les propriétés, et exposer createdAt ou role dans une liste publique est inutile.

typescripttype PublicUser = Pick<User, "id" | "name">

function toPublicUser(user: User): PublicUser {
  return { id: user.id, name: user.name }
}

Omit

L'inverse de Pick. Retire des propriétés :

typescripttype CreateUser = Omit<User, "id" | "createdAt">
// {
//   name: string
//   email: string
//   role: "admin" | "user"
// }

Utile pour la création d'entités ou l'id est généré cote serveur :

typescriptasync function createUser(data: Omit<User, "id" | "createdAt">): Promise<User> {
  return prisma.user.create({ data })
}

Pick vs Omit : lequel choisir ?

Ma regle : si tu gardes plus de propriétés que tu en retires, utilise Omit. Si tu retires plus que tu gardes, utilise Pick.

typescript// User a 5 proprietes, je veux en garder 2 → Pick
type Summary = Pick<User, "name" | "email">

// User a 5 proprietes, je veux en retirer 1 → Omit
type CreateUser = Omit<User, "id">

Record

Cree un type objet avec des clés de type K et des valeurs de type V :

typescripttype Roles = "admin" | "editor" | "viewer"

const permissions: Record<Roles, string[]> = {
  admin: ["read", "write", "delete"],
  editor: ["read", "write"],
  viewer: ["read"]
}

Record garantit que toutes les clés de l'union sont presentes. Si tu ajoutes "moderator" a Roles, le compilateur reclame l'entree manquante dans permissions.

Autres usages courants :

typescript// Dictionnaire generique
type Dict<T> = Record<string, T>
const cache: Dict<User> = {}

// Mapping de codes HTTP
const statusMessages: Record<number, string> = {
  200: "OK",
  404: "Not Found",
  500: "Internal Server Error"
}

Readonly

Deja couvert dans l'article sur l'immutabilité, mais c'est bien un utility type :

typescripttype ImmutableUser = Readonly<User>
// Toutes les proprietes deviennent readonly

Exclude et Extract

Ces deux-la travaillent sur des types union, pas sur des objets.

typescripttype Status = "active" | "inactive" | "banned" | "deleted"

type ActiveStatus = Exclude<Status, "banned" | "deleted">
// "active" | "inactive"

type DangerStatus = Extract<Status, "banned" | "deleted">
// "banned" | "deleted"

Exclude retire les membres de l'union qui sont assignables a U. Extract garde uniquement ceux qui sont assignables a U.

Utile pour créer des sous-ensembles de valeurs :

typescripttype AllEvents = "click" | "scroll" | "keydown" | "keyup" | "focus" | "blur"
type KeyboardEvents = Extract<AllEvents, "keydown" | "keyup">
type NonKeyboardEvents = Exclude<AllEvents, KeyboardEvents>

ReturnType et Parameters

Extraient le type de retour et les paramètres d'une fonction :

typescriptfunction createUser(name: string, email: string): User {
  // ...
}

type CreateReturn = ReturnType<typeof createUser> // User
type CreateParams = Parameters<typeof createUser> // [string, string]

ReturnType est utile quand tu veux typer une variable avec le retour d'une fonction sans importer le type explicitement :

typescript// La fonction vient d'une lib, son type de retour est complexe
import { parseConfig } from "some-lib"

type Config = ReturnType<typeof parseConfig>

Parameters retourne un tuple des types des paramètres. Tu peux acceder a un paramètre spécifique par index :

typescripttype FirstParam = Parameters<typeof createUser>[0] // string

Awaited

Deballe le type d'une Promise (recursivement) :

typescripttype A = Awaited<Promise<string>> // string
type B = Awaited<Promise<Promise<number>>> // number
type C = Awaited<string> // string (pas une Promise, retourne tel quel)

Utile pour typer le résultat d'un await sur une fonction async :

typescriptasync function fetchUsers(): Promise<User[]> {
  // ...
}

type Users = Awaited<ReturnType<typeof fetchUsers>> // User[]

Combinaisons courantes

Les utility types se combinent. Quelques patterns que j'utilise sur paltemps.fr :

typescript// Creation : tout sauf id et timestamps
type CreateInput<T> = Omit<T, "id" | "createdAt" | "updatedAt">

// Update partiel : creation mais tout optionnel
type UpdateInput<T> = Partial<CreateInput<T>>

// Reponse API : l'entite complete en readonly
type ApiEntity<T> = Readonly<T>

// Mapping d'un enum a des configs
type FeatureFlags = Record<Feature, boolean>

Tu peux les chainer :

typescript// Prend name et email de User, rend tout optionnel et readonly
type UserPatch = Readonly<Partial<Pick<User, "name" | "email">>>

Mais si ca devient illisible, créé un type nomme. La lisibilité compte plus que l'elegance.


Résumé

  • Partial rend tout optionnel, Required rend tout obligatoire
  • Pick garde des propriétés, Omit en retire
  • Record créé un mapping clés → valeurs avec vérification d'exhaustivite
  • Exclude et Extract filtrent des types union
  • ReturnType et Parameters extraient les types d'une fonction
  • Les utility types se combinent et derivent tous d'un type source unique

Article précédent : 09 - Null safety

Article suivant : 11 - Union vs intersection

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.