Maîtrisez les types utilitaires intégrés de TypeScript pour un code plus propre
Découvrez comment les types utilitaires de TypeScript tels que Partial, Pick, Omit et Record éliminent les interfaces redondantes et maintiennent automatiquement les définitions de types en synchronisation.
Cessez de copier-coller des interfaces : écrivez du code plus propre et fiable grâce aux transformateurs de types intégrés à TypeScript.
Tôt ou tard, tout développeur utilisant TypeScript se heurte au même problème. Vous concevez une interface User claire composée de huit champs. Quelques minutes plus tard, vous avez besoin d’un formulaire de modification où seuls deux champs sont réellement obligatoires, alors vous créez une interface UpdateUserInput. Ensuite, une réponse API exige que des champs sensibles soient cachés, ce qui vous pousse à ajouter également une interface PublicUser. Avant même que vous vous en rendiez compte, une grande partie de votre base de code est constituée d’interfaces presque identiques qui finissent par ne plus être synchronisées à mesure que le projet progresse.
Chaque fois que le schéma de base change, vous êtes contraint de rechercher et de mettre à jour plusieurs endroits en même temps. En oubliant un seul, vous créez simplement une erreur qui apparaîtra plus tard en production.
TypeScript résout ce problème grâce à un ensemble d’outils intégré appelé Utility Types. On peut les considérer comme des fonctions qui opèrent sur des types plutôt que sur des valeurs : on fournit un type existant, on applique une règle de transformation, et on obtient un nouveau type. Cela permet de conserver une définition unique officielle et de déléguer tout le travail de transformation répétitif au compilateur.
Ci-dessous sont présentés les types utilitaires à utiliser fréquemment, expliqués en langage simple avec des exemples concrets que vous pouvez adapter immédiatement.
1. Transformer la stricteur des propriétés
Ces trois types utilitaires permettent de déterminer si les champs sont optionnels, obligatoires ou figés.
Partial<Type>
En termes simples : « Rendre chaque propriété optionnelle. »
Lorsque vous créez un point de terminaison comme PATCH ou que vous mettez à jour une partie de l’état d’une application, vous ne souhaitez pas exiger l’ensemble de l’objet auprès de l’appelant. Partial<T> parcourt chaque champ d’une interface et ajoute un ? à chacun.
interface UserProfile {
id: string;
name: string;
email: string;
avatarUrl: string;
theme: "light" | "dark";
}
// Without Partial, you'd have to rewrite all fields with '?'
type UpdateProfileInput = Partial<UserProfile>;
function updateProfile(userId: string, changes: UpdateProfileInput) {
// Valid: changes can include any subset of UserProfile properties
apiClient.patch(`/users/${userId}`, changes);
}
// Valid call:
updateProfile("usr_101", { theme: "dark" });
Required<Type>
En termes simples : "Supprimez tous les marqueurs optionnels afin qu’aucun élément ne puisse être omis."
Required<T> fait le contraire de Partial<T> : étant donné une interface où certains champs sont optionnels, il force leur présence à tous. Cela est utile lorsque vous intégrez des paramètres par défaut et que vous devez garantir qu’aucun élément ne manque.
interface AppConfig {
apiUrl: string;
timeout?: number;
retries?: number;
enableLogging?: boolean;
}
// Ensure the internal runner has every single setting resolved
type ResolvedConfig = Required<AppConfig>;
const defaultConfig: ResolvedConfig = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3,
enableLogging: true,
};
function initializeApp(userOptions: AppConfig): ResolvedConfig {
return { ...defaultConfig, ...userOptions };
}
Readonly<Type>
En termes simples : « Geler chaque propriété afin qu’elle ne puisse pas être réaffectée. »
Lorsque vous souhaitez éviter des modifications accidentelles — pensez aux valeurs de configuration, aux propriétés des composants ou à l’état partagé de l’application — Readonly<T> marque chaque propriété comme readonly. Toute tentative d’écrire dans l’une de ces propriétés échoue au moment de la compilation.
interface SystemRole {
roleName: string;
permissions: string[];
}
const SuperAdminRole: Readonly<SystemRole> = {
roleName: "SUPER_ADMIN",
permissions: ["read", "write", "delete", "admin"],
};
// Error: Cannot assign to 'roleName' because it is a read-only property.
SuperAdminRole.roleName = "USER";
2. Sélectionner ce dont vous avez besoin
Il existe de nombreuses situations où vous n’avez besoin que d’une partie d’une interface. Au lieu d’écrire manuellement un type entièrement nouveau et non lié, vous pouvez extraire directement des parties de vos modèles de domaine existants.
Pick<Type, Keys>
En termes simples : « Créer un nouveau type en utilisant uniquement ces clés nommées de l’original. »
Disons que vous disposez d’un modèle volumineux, mais qu’un widget d’interface utilisateur de petite taille ou une requête de base de données légère ne se soucie que d’un nom et d’un avatar. Vous pouvez indiquer à TypeScript de conserver uniquement ces deux champs.
interface Product {
id: string;
sku: string;
name: string;
price: number;
stockQuantity: number;
supplierId: string;
description: string;
}
// Pick only the fields a mini checkout card displays
type CartItemPreview = Pick<Product, "id" | "name" | "price">;
const item: CartItemPreview = {
id: "prod_99",
name: "Wireless Mechanical Keyboard",
price: 129.99,
};
Omettre <Type, Keys>
En termes simples : « Prendre le type d’origine complet, mais supprimer ces champs en particulier. »
Omettre <T, K> inverse la fonction de Pick. Lorsque vous insérez un nouveau enregistrement dans une base de données, vous disposez généralement de tous les champs sauf ceux que le système génère automatiquement, tels que id ou createdAt.
interface Article {
id: number;
title: string;
slug: string;
content: string;
publishedAt: Date;
viewCount: number;
}
// Strip metadata handled by the database
type CreateArticlePayload = Omit<Article, "id" | "viewCount" | "publishedAt">;
function submitArticle(payload: CreateArticlePayload) {
// TypeScript guarantees no one accidentally submits an 'id' or 'viewCount'
apiClient.post("/articles", payload);
}
3. Mappage dynamique des clés
Record<Keys, Type>
En termes simples : « Créez un dictionnaire dont les clés proviennent de cette liste et dont les valeurs suivent cette structure. »
Les objets JavaScript classiques utilisés comme tables de recherche ont souvent un type flou, du genre { [key: string]: any }, ce qui met en péril la sécurité des types. Record<Keys, Type>, quant à lui, définit précisément quels sont les types de clés autorisés et quelle forme doivent avoir leurs valeurs.
type SubscriptionTier = "free" | "pro" | "enterprise";
interface TierFeatures {
monthlyQuota: number;
customDomainAllowed: boolean;
supportLevel: "community" | "email" | "dedicated";
}
// Guarantees all three tiers are explicitly handled
const subscriptionPlans: Record<SubscriptionTier, TierFeatures> = {
free: {
monthlyQuota: 1000,
customDomainAllowed: false,
supportLevel: "community",
},
pro: {
monthlyQuota: 50000,
customDomainAllowed: true,
supportLevel: "email",
},
enterprise: {
monthlyQuota: 1000000,
customDomainAllowed: true,
supportLevel: "dedicated",
},
};
Si un nouveau niveau « starter » est ajouté à SubscriptionTier plus tard, TypeScript signalera immédiatement subscriptionPlans et vous indiquera que l’entrée « starter » manque.
4. Réduction et affinage des types union
Les outils ci-dessus permettent de redéfinir les clés des objets. La paire suivante, quant à elle, fonctionne sur des types union, tels que "apple" | "banana" | "orange".
Exclude<UnionType, ExcludedMembers>
En termes simples : "Prendre cet ensemble de valeurs possibles et en supprimer celles listées ici."
type TaskStatus = "draft" | "in_progress" | "review" | "completed" | "archived";
// You can edit tasks in any status except when they are completed or archived
type EditableTaskStatus = Exclude<TaskStatus, "completed" | "archived">;
// Result: "draft" | "in_progress" | "review"
Extract<UnionType, ExtractedMembers>
En termes simples : "Prendre cet ensemble de valeurs possibles et n’en conserver que celles qui correspondent à ces critères."
Extract fonctionne dans le sens inverse de Exclude : il isole la partie commune entre deux types union, ne conservant que les membres présents dans les deux.
type WindowEvents = "click" | "scroll" | "mousemove" | "keydown" | "keyup";
type PointerEvents = "click" | "mouseenter" | "mouseleave" | "mousemove";
// Keep only the events shared across both categories
type SharedEvents = Extract<WindowEvents, PointerEvents>;
// Result: "click" | "mousemove"
NonNullable<Type>
En termes simples : éliminer les valeurs null et undefined d’un type.
Lorsque l’on travaille avec des données provenant d’API externes ou de clients de base de données, les champs sont souvent définis comme string | null | undefined. NonNullable<T> élimine ces possibilités de valeurs vides pour ne conserver que les types significatifs.
type SearchQuery = string | string[] | null | undefined;
type CleanSearchQuery = NonNullable<SearchQuery>;
// Result: string | string[]
5. Déduire les types à partir du code existant
L’un des plus grands avantages de TypeScript est sa capacité à extraire directement les types des fonctions, des promesses et des modules importés, sans avoir à les déclarer manuellement.
ReturnType<FunctionType>
En termes simples : il indique ce que renvoie une fonction, vous évitant ainsi de devoir réécrire manuellement cette structure.
Pour les fonctions d’usine complexes, les sélecteurs de style Redux, ou les packages tiers qui ne révèlent pas leur structure de retour, vous pouvez obtenir la structure exacte en faisant référence à la fonction elle-même.
function buildSessionPayload(userId: string, roles: string[]) {
return {
sessionId: crypto.randomUUID(),
authenticatedAt: Date.now(),
expiresAt: Date.now() + 1000 * 60 * 60 * 24,
account: {
id: userId,
primaryRole: roles[0] ?? "guest",
allRoles: roles,
},
};
}
// Automatically inherits the shape of whatever buildSessionPayload returns
type UserSession = ReturnType<typeof buildSessionPayload>;
Si l’objet retourné à l’intérieur de buildSessionPayload change par la suite, UserSession prend en compte ce changement automatiquement.
Parameters<FunctionType>
En termes simples : il génère une tuple listant tous les arguments acceptés par une fonction.
Chaque fois que vous enveloppez une méthode SDK, que vous redirigez des arguments ou que vous créez une couche de journalisation autour d’un appel de fonction, Parameters<T> garantit que la signature de votre enveloppe ne s’éloigne jamais de celle de l’originale.
function triggerAlert(message: string, severity: "low" | "high", code?: number) {
// Internal dispatch logic
}
// Extracts arguments as a tuple: [message: string, severity: "low" | "high", code?: number]
type AlertArgs = Parameters<typeof triggerAlert>;
function logAndAlert(...args: AlertArgs) {
console.log("Dispatching alert:", args[0]);
triggerAlert(...args);
}
Awaited<Type>
En termes simples : il déballle une Promise et expose la valeur qu’elle contient.
Puisque tant de concepts modernes de TypeScript tournent autour de code asynchrone, Awaited<T> est essentiel pour enlever une ou plusieurs couches de promises imbriquées jusqu’à atteindre la valeur réellement résolue.
async function fetchAccountDetails() {
return {
accountId: "acct_8472",
balance: 4250.75,
currency: "USD",
};
}
// Unwraps the Promise<T> returned by the async function
type AccountDetails = Awaited<ReturnType<typeof fetchAccountDetails>>;
// Result: { accountId: string; balance: number; currency: string }
const cachedAccount: AccountDetails = {
accountId: "acct_8472",
balance: 4250.75,
currency: "USD",
};
6. Empiler des types utilitaires comme des blocs de construction
Les types utilitaires montrent toute leur puissance lorsqu’ils sont combinés. Comme chacun d’eux produit un type standard de TypeScript, vous pouvez passer directement le résultat d’un dans l’autre.
Considérez une couche de données construite autour d’une entité Client :
interface Customer {
id: string;
firstName: string;
lastName: string;
email: string;
phoneNumber?: string;
createdAt: Date;
updatedAt: Date;
}
// 1. Creation payload: No auto-generated fields, but firstName, lastName, and email are mandatory
type CreateCustomerDTO = Omit<Customer, "id" | "createdAt" | "updatedAt">;
// 2. Update payload: Omit IDs, and let the user modify ANY valid field optionally
type UpdateCustomerDTO = Partial<Omit<Customer, "id" | "createdAt" | "updatedAt">>;
// 3. Read-only view for UI caches
type ReadonlyCustomer = Readonly<Customer>;
Avec seulement quelques lignes de code, vous avez créé trois objets de transfert de données distincts et entièrement vérifiés au niveau typologique. Si l’interface de base Customer acquiert ultérieurement une nouvelle propriété — disons loyaltyTier — les trois types dérivés s’actualisent automatiquement, sans aucune modification manuelle requise.
Résumé de référence rapide
Considérer les types comme des duplicatas copiés à la main fait de chaque modification du schéma une charge de maintenance. Les types utilitaires vous permettent de définir vos modèles métier une seule fois, puis le compilateur génère toutes les variations nécessaires à partir de cette définition unique. Le résultat est un code plus léger, des refacturations plus sûres, et moins de temps consacré à harmoniser des interfaces presque identiques, vous permettant ainsi de vous concentrer sur le développement de fonctionnalités.