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é
Partialrend tout optionnel,Requiredrend tout obligatoirePickgarde des propriétés,Omiten retireRecordcréé un mapping clés → valeurs avec vérification d'exhaustiviteExcludeetExtractfiltrent des types unionReturnTypeetParametersextraient 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