Utility Types dans TypeScript: Partial, Pick, Omit, Record

1. Aperçu

Quand vous écrivez du TypeScript, vous rencontrerez des situations où vous avez besoin d’un type similaire à un type existant mais avec quelques champs en moins ou en plus. Au lieu de déclarer une nouvelle interface, TypeScript fournit déjà un groupe de utility types pour transformer les types existants. Cet article passe en revue les quatre utility types les plus utilisés : Partial, Pick, Omit, Record, avec des exemples pratiques et quelques erreurs courantes. Après avoir lu, vous saurez choisir le bon utility type pour chaque situation au lieu de réécrire les interfaces manuellement.

2. Partial<T>

Partial<T> rend tous les champs de T optionnels. Utile lors de l’écriture de fonctions de mise à jour, où l’appelant ne doit transmettre que certains champs.

interface Customer {
  id: number;
  name: string;
  email: string;
  phone: string;
}

function updateCustomer(id: number, changes: Partial<Customer>): Customer {
  const current = findCustomerById(id); // on suppose que cette fonction existe
  return { ...current, ...changes };
}

updateCustomer(1, { email: "new@mail.com" });

Partial<Customer> crée un type équivalent :

interface PartialCustomer {
  id?: number;
  name?: string;
  email?: string;
  phone?: string;
}

Grâce à cela, updateCustomer accepte un objet avec seulement email sans signaler une erreur de champ manquant.

Remarque : Partial rend seulement optionnel au niveau 1 (shallow). Si Customer a un champ qui est un objet imbriqué, les champs imbriqués restent obligatoires. Pour un partial profond, vous devez écrire vous-même un type récursif ou utiliser une bibliothèque comme type-fest.

3. Pick<T, K>

Pick<T, K> sélectionne un sous-ensemble de champs de T selon la liste de clés K. À utiliser quand vous n’avez besoin que d’une partie des données d’un type volumineux, par exemple un DTO renvoyé au client.

interface Order {
  id: string;
  customerId: number;
  totalAmount: number;
  items: string[];
  createdAt: Date;
  internalNote: string;
}

type OrderSummary = Pick<Order, "id" | "totalAmount" | "createdAt">;

const summary: OrderSummary = {
  id: "ORD-001",
  totalAmount: 250_000,
  createdAt: new Date(),
};

OrderSummary ne contient que 3 champs. Si vous ajoutez les champs items ou internalNote à summary, le compilateur signalera l’erreur Object literal may only specify known properties.

3.1. Pick combiné avec des clés union

Les clés passées à Pick sont des union de string littérales, vous pouvez donc les extraire dans une variable séparée pour les réutiliser :

type OrderPublicFields = "id" | "totalAmount" | "createdAt";

type OrderSummary = Pick<Order, OrderPublicFields>;
type OrderListItem = Pick<Order, OrderPublicFields | "customerId">;

Cette approche évite de répéter la liste des clés à plusieurs endroits quand vous avez plusieurs variantes de DTO provenant de la même entité.

4. Omit<T, K>

Omit<T, K> fait l’inverse de Pick : il supprime les clés spécifiées et conserve le reste. À utiliser généralement quand vous voulez masquer des champs sensibles ou des champs internes.

interface User {
  id: number;
  username: string;
  passwordHash: string;
  role: string;
}

type PublicUser = Omit<User, "passwordHash">;

function toPublicUser(user: User): PublicUser {
  const { passwordHash, ...rest } = user;
  return rest;
}

La ligne const { passwordHash, ...rest } = user sépare passwordHash, et rest ne contient que id, username, role — correspond exactement au type PublicUser.

// Résultat quand console.log(toPublicUser(user))
{ id: 1, username: "nonla_dev", role: "ADMIN" }

Remarque : Omit est défini à partir de Pick et Exclude, fonctionne bien avec les types de littéraux d’objet mais ne supprime pas correctement les surcharges ou les union discriminées dans certains cas complexes. Avec les types union (type A = B | C), Omit<A, K> fusionnera les membres avant de faire l’omission, ce qui peut perdre la propriété discriminante. Quand vous rencontrez une union complexe, envisagez d’écrire le type manuellement au lieu d’utiliser Omit directement.

5. Record<K, T>

Record<K, T> crée un type d’objet avec toutes les clés appartenant à K, chaque clé ayant une valeur de type T. Utile pour modéliser une map/dictionnaire avec des clés fixes.

type PaymentStatus = "PENDING" | "PAID" | "CANCELLED";

interface StatusInfo {
  label: string;
  color: string;
}

const statusDisplay: Record<PaymentStatus, StatusInfo> = {
  PENDING: { label: "En attente de paiement", color: "orange" },
  PAID: { label: "Payé", color: "green" },
  CANCELLED: { label: "Annulé", color: "gray" },
};

function getStatusLabel(status: PaymentStatus): string {
  return statusDisplay[status].label;
}

Puisque statusDisplay déclare le type Record<PaymentStatus, StatusInfo>, le compilateur vous oblige à avoir les trois clés PENDING, PAID, CANCELLED. Si vous oubliez une clé, TypeScript signalera une erreur au moment de la compilation — c’est un avantage par rapport à l’utilisation de { [key: string]: StatusInfo } habituelle (la signature d’index ne vérifie pas les clés suffisantes).

5.1. Record avec des clés dynamiques

Quand la clé n’est pas une union littérale mais une string générale, Record<string, T> est équivalent à une signature d’index :

const cache: Record<string, number> = {};
cache["order-1"] = 100;

Dans ce cas, il n’y a plus de vérification « des clés suffisantes », car string peut avoir un nombre infini de valeurs. Utilisez Record<string, T> seulement quand vous avez vraiment besoin d’un dictionnaire flexible, sinon privilégiez les union littérales pour bénéficier de la vérification exhaustive.

6. Kết hợp nhiều utility type

Les utility types peuvent être imbriqués les uns dans les autres pour créer des types plus complexes sans avoir besoin d’écrire une nouvelle interface.

interface Product {
  id: string;
  name: string;
  price: number;
  stock: number;
  supplierNote: string;
}

type ProductUpdateRequest = Partial<Omit<Product, "id">>;

function updateProduct(id: string, data: ProductUpdateRequest) {
  // appeler repository.update(id, data)
}

updateProduct("P001", { price: 199_000 });

Omit<Product, "id"> supprime le champ id (vous ne pouvez pas modifier la clé primaire), puis Partial rend les champs restants optionnels. Résultat : l’appelant ne doit transmettre que les champs qu’il souhaite modifier, il n’est pas autorisé à transmettre id.

7. Vitre des tests pour vérifier le comportement

Puisque les utility types n’existent que au moment de la compilation, la façon concrète de les vérifier est d’écrire des tests unitaires pour les fonctions qui utilisent ces types, pour s’assurer que le comportement à l’exécution est correct. Par exemple avec Vitest :

<!-- dépendance équivalente du package.json (pas Maven) -->
import { describe, it, expect } from "vitest";

interface User {
  id: number;
  username: string;
  passwordHash: string;
  role: string;
}

type PublicUser = Omit<User, "passwordHash">;

function toPublicUser(user: User): PublicUser {
  const { passwordHash, ...rest } = user;
  return rest;
}

describe("toPublicUser", () => {
  it("supprime passwordHash du résultat", () => {
    const user: User = {
      id: 1,
      username: "nonla_dev",
      passwordHash: "hash-123",
      role: "ADMIN",
    };

    const result = toPublicUser(user);

    expect(result).toEqual({
      id: 1,
      username: "nonla_dev",
      role: "ADMIN",
    });
    expect(result).not.toHaveProperty("passwordHash");
  });
});

Ce test confirme que la fonction toPublicUser est non seulement correcte au niveau du type, mais aussi au moment de l’exécution — passwordHash disparaît réellement de l’objet renvoyé, ce n’est pas seulement « masqué » au niveau du type.

8. Erreurs courantes et quand ne pas utiliser

  • Confondre Pick/Omit avec la suppression de données réelles : tous deux changent seulement le type, n’affectent pas l’objet à l’exécution. La destructuration (const { x, ...rest } = obj) est ce qui supprime réellement le champ des données.
  • Abuser de Partial pour les objets renvoyés par l’API : si la réponse contient toujours tous les champs, utiliser Partial obligera le code appelant à vérifier undefined inutilement partout. Utilisez Partial seulement pour l’entrée (update/patch), ne l’utilisez pas pour la sortie qui est certainement complète.
  • Omit avec les types union complexes : comme mentionné à la section 4, Omit peut perdre la propriété du union discriminé. Si votre type est une union de plusieurs variantes (par exemple Shape = Circle | Square), écrivez le type vous-même ou utilisez une technique de type mappé avec condition au lieu d’utiliser Omit directement.
  • Record<string, T> au lieu d’une interface explicite : si vous connaissez d’avance toutes les clés, déclarez une union littérale et utilisez Record<Key, T> pour avoir une vérification exhaustive. Utiliser Record<string, T> arbitrairement perdra la capacité à détecter les erreurs de clés manquantes/excédentaires au moment de la compilation.

Remarque : Tous ces utility types ne fonctionnent qu’au moment de la compilation. Après la compilation en JavaScript, ils disparaissent complètement — pas de surcharge à l’exécution, mais il n’y a pas non plus de validation à l’exécution. Si les données proviennent de l’extérieur (API, fichier JSON), vous devez toujours valider avec une bibliothèque comme Zod ou io-ts.

9. Conclusion

Partial, Pick, Omit et Record vous aident à réutiliser les types au lieu de réécrire des interfaces dupliquées. Partial convient pour les entrées de type mise à jour, Pick/Omit sont utilisés pour créer des DTO à partir d’une grande entité, tandis que Record est idéal pour les dictionnaires avec des clés fixes, tirer parti de la vérification exhaustive du compilateur. Comprendre les limites de chaque type — en particulier le comportement shallow de Partial et le problème des types union d’Omit — vous aidera à éviter les erreurs de type difficiles à déboguer à l’avenir.

typescriptutility-typestype-systemfrontend