Sébastien TIMONER
Expert en développement web et gestion d'équipes techniques, je me spécialise dans la création et l'optimisation de solutions numériques performantes. Grâce à une maîtrise approfondie de technologies modernes comme React.js, Node.js, TypeScript, Symfony, Docker et FrankenPHP, j'assure la réussite de projets SaaS complexes, de la conception à la mise en production, pour des entreprises de divers secteurs.
Les utility types de TypeScript sont des types génériques pré-définis qui facilitent les transformations de types. Ils permettent de créer de nouveaux types à partir de types existants, par exemple en rendant certaines propriétés optionnelles ou en extrayant le type de retour d'une fonction. Ce guide pédagogique, en trois niveaux (débutant, intermédiaire, avancé), présente tous les utility types standards de TypeScript (tels que Partial, Required, Readonly, Pick, Omit, Record, Exclude, Extract, NonNullable, Parameters, ReturnType, InstanceType, ThisType, etc.), avec pour chacun : une explication de son utilité, un exemple d'utilisation en TypeScript, puis une comparaison avec une approche équivalente sous Zod, une bibliothèque de schémas de validation runtime. Nous discuterons également des différences de philosophie entre les types utilitaires de TypeScript (système de types statique) et les schémas Zod (validation à l'exécution).
Pour essayer les extraits de code, vous pouvez initialiser un projet TypeScript minimal en utilisant Bun (un exécuteur JavaScript ultra-rapide qui fait aussi office de gestionnaire de paquets). Assurez-vous d'avoir Bun installé, puis dans un dossier vide exécutez :
shell
Bun créera un package.json et un tsconfig.json par défaut. Vous pouvez ensuite exécuter un fichier TypeScript avec bun run fichier.ts (Bun transpile TypeScript à la volée).
Nous sommes prêts à explorer les utility types !
TypeScript : L'utility type Partial<T> construit un type à partir d'un type existant T en rendant toutes ses propriétés optionnelles . Autrement dit, chaque propriété de T devient facultative (ajout d'un ?). Cela permet de représenter des objets partiels de ce type. Par exemple, si l'on a un type User avec des champs obligatoires, Partial<User> permettra d'en créer une version où ces champs peuvent être omis :
typescript
Ici, Partial<User> a le type { name?: string; age?: number }. On peut ainsi, par exemple, implémenter une fonction de mise à jour partielle sans exiger tous les champs :
typescript
On appelle updateUser(existingUser, { age: 26 }) avec un objet partiel – TypeScript garantit statiquement que fieldsToUpdate ne contient que des clés valides de User.
Equivalent avec Zod : Zod offre une méthode équivalente pour rendre un schéma d'objet partiel. La méthode .partial() appliquée à un schéma z.object rend toutes les clés optionnelles, à l'instar de Partial en TypeScript . Par exemple :
typescript
Ici PartialUserSchema est un schéma qui valide les objets ne contenant éventuellement qu'un sous-ensemble des clés (name et age deviennent facultatifs). On peut vérifier son fonctionnement :
typescript
Zod permet aussi de rendre seulement certaines propriétés optionnelles en passant un objet en paramètre (UserSchema.partial({ age: true }) ne rend optionnel que age ). À noter, .partial() n'agit que sur le premier niveau ; Zod fournit aussi .deepPartial() pour rendre optionnels récursivement les champs imbriqués .
TypeScript : L'utility type Required<T> est l'inverse de Partial : il crée un type à partir de T en rendant toutes ses propriétés obligatoires (non-optionnelles) . Il est utile pour « solidifier » un type dont certaines propriétés étaient optionnelles. Par exemple :
typescript
Dans cet exemple, Required<Settings> est équivalent à { theme: string; fontSize: number } – toute omission de propriété devient une erreur de compilation.
Equivalent avec Zod : Pour un schéma Zod, l'équivalent est la méthode .required(), qui transforme un schéma d'objet en rendant toutes ses propriétés requises (contrairement à .partial() qui les rend optionnelles) . Cette méthode est surtout utile si l'on a d'abord défini un schéma partiel et qu'on souhaite en obtenir la version stricte. Par exemple :
typescript
Ici StrictUserSchema exige que name et age soient présents. On peut aussi cibler certaines propriétés : BaseUserSchema.required({ age: true }) rend seulement age requis (et laisse name optionnel) .
Notez que Required<T> agit au temps de compilation (il n'existe plus dans le code JavaScript émis), alors que schema.required() de Zod agit à l'exécution en validant la présence des clés lors du parse.
TypeScript : L'utility Readonly<T> transforme un type T en rendant ses propriétés en lecture seule (immutables) . Toute tentative de réassigner ces propriétés sera signalée comme erreur à la compilation. Par exemple :
typescript
Ici, todo est de type Readonly<Todo> donc ses champs ne peuvent être modifiés après l'initialisation. Cela correspond souvent à l'utilisation de Object.freeze en JavaScript, mais TypeScript ne gère cela qu'au niveau du type (il n'empêche pas réellement la mutation à l'exécution, il avertit seulement au moment de la compilation).
Equivalent avec Zod : Zod propose une fonctionnalité intéressante pour l'immuabilité. La méthode .readonly() appliquée à un schéma retourne un nouveau schéma qui, lors du parsing, appelle Object.freeze() sur le résultat, et dont le type statique est marqué Readonly<...> . Autrement dit, Zod peut figer l'objet validé et garantir qu'il correspond à un type readonly. Par exemple :
typescript
Ici, FrozenUserSchema fige l'objet en sortie de parse. Si vous essayez de modifier frozenUser, une exception sera levée à l'exécution car l'objet est immuable. Ainsi, Zod va plus loin que TypeScript : là où Readonly<T> est purement statique (les propriétés pourraient être modifiées en JS si on contournait le typage), le schéma Zod .readonly() assure l'immuabilité réelle à l'exécution en plus de fournir le type Readonly<...> correspondant.
TypeScript : Pick<T, Keys> construit un nouveau type à partir de T en ne conservant que certaines propriétés spécifiées par Keys (généralement une union de littéraux de strings représentant les noms de propriétés) . C'est utile pour créer un type plus restreint à partir d'un type existant. Par exemple :
typescript
Ici, TodoPreview ne conserve que title et completed de FullTodo. Toute propriété non listée (description par ex.) est exclue du type résultant.
Equivalent avec Zod : Les schémas Zod d'objets possèdent aussi une méthode .pick() qui crée un nouveau schéma ne contenant que les clés indiquées , de manière semblable :
typescript
On passe un objet avec les clés à conserver mises à true. TodoPreviewSchema ne validera que des objets comportant title et completed (les autres propriétés de FullTodo seront ignorées par le parseur par défaut). Par exemple, TodoPreviewSchema.parse({ title: "Nettoyer", completed: false, description: "..." }) retournera { title: "...", completed: false } en ayant retiré description (car par défaut Zod ignore les clés non spécifiées, comportement qu'on peut ajuster avec .strict() ou .passthrough() selon le besoin).
De manière équivalente, Zod fournit .omit() pour l'effet inverse.
TypeScript : Omit<T, Keys> crée un nouveau type en partant de T et en supprimant certaines propriétés (Keys) . C'est le complément de Pick (d'ailleurs Omit<T, K> est souvent implémenté comme Pick<T, Exclude<keyof T, K>>). Exemple :
typescript
Ici TaskMeta ne contient plus que id et title. Les propriétés done et dueDate ont été exclues.
Equivalent avec Zod : La méthode .omit() de Zod produit un schéma d'objet sans les clés indiquées . Exemple :
typescript
TaskMetaSchema validera des objets ne comportant que id et title. Toute clé omise (done, dueDate) sera ignorée si présente en entrée. Ainsi, Pick et Omit permettent de projeter un schéma Zod sur un sous-ensemble de champs tout comme leurs équivalents TypeScript le font au niveau des types.
TypeScript : Record<Keys, Type> construit un type d'objet dont les clés sont de type Keys (généralement une union de littéraux de string ou un type string/number plus général) et les valeurs sont de type Type . Cela permet de décrire, par exemple, un objet indexé par des identifiants. Un exemple classique :
typescript
Ici, Record<CatName, CatInfo> garantit que l'objet cats a exactement les clés "miffy", "boris", "mordred", chacune associée à un CatInfo. Record est très utile pour typer des objets utilisés comme dictionnaires ou maps.
Equivalent avec Zod : Zod offre le schéma z.record(keySchema, valueSchema) pour valider des objets de ce genre . On peut l'utiliser avec un schéma de clé (par exemple z.string() ou un schéma de littéraux) et un schéma de valeur. Exemple :
typescript
Ici, AgeMapSchema valide n'importe quelle clé tant que c'est une string (par défaut, Zod z.record sans premier argument suppose string). Si vous avez un ensemble fini de clés, il est souvent préférable d'utiliser z.object({ ... }) avec des clés explicitement nommées ou z.enum([...]) pour contraindre les clés. D'ailleurs, Zod autorise un schéma de clés plus strict : on pourrait définir const CatNameSchema = z.enum(["miffy","boris","mordred"]) puis faire z.record(CatNameSchema, CatInfoSchema) – cependant, sachez qu'à l'exécution les clés d'objets JavaScript sont toujours des chaînes de caractères. Même une clé numérique sera convertie en string au runtime (par ex., une clé 1 devient "1" dans un objet JS) . Zod prend cela en compte et ne permet pas de définir un schéma de clé numérique pur sans le transformer en string.
En résumé, Record<Keys, Type> (TypeScript) et z.record(keySchema, valueSchema) (Zod) permettent tous deux de typer des dictionnaires. Le premier agit au niveau du système de types, le second à l'exécution pour valider la structure de l'objet.
TypeScript : Exclude<U, E> crée un type en enlevant de l'union U tous les membres qui sont assignables au type E . En d'autres termes, on exclut certains types d'une union. Par exemple :
typescript
Ici, Exclude<Role, "admin"> retire le littéral "admin" de l'union, ne laissant que "user" | "guest". Un autre exemple : Exclude<string | number | boolean, string> donnerait number | boolean (on retire tout ce qui est assignable à string). C'est utile pour affiner un type union en éliminant des cas indésirables.
Equivalent avec Zod : Pour restreindre les possibilités d'une union au runtime, on définit généralement un schéma n'incluant pas les cas à exclure. Par exemple, si l'on a un schéma RoleSchema = z.enum(["admin","user","guest"]), on peut créer un sous-ensemble en n'incluant pas "admin". Zod fournit justement pour les énumérations la méthode `.exclude([…]) qui génère un nouveau schéma d'énumération sans les valeurs spécifiées . Par exemple :
typescript
Ce schéma NonAdminSchema correspond exactement au type Exclude<Role, "admin"> mais assure en plus la validation à l'exécution. Sous le capot, RoleSchema.exclude(["admin"]) retire simplement "admin" de la liste des littéraux autorisés. De même, Zod propose .extract([...]) pour effectuer l'opération complémentaire.
TypeScript : Extract<T, U> crée un type en conservant de T uniquement les membres assignables à U . C'est grosso modo l'inverse d'Exclude. Par exemple :
typescript
Autre exemple : Extract<"a" | "b" | "c", "a" | "f"> donnera "a" (car seul "a" est commun aux deux). Ce utility type sert donc à filtrer une union pour n'en extraire qu'un certain sous-type.
Equivalent avec Zod : Comme pour Exclude, Zod permet d'obtenir au runtime un schéma correspondant à un sous-ensemble d'une énumération via .extract([...]) . Reprenons l'exemple des rôles : pour extraire uniquement, disons, les rôles limités "user" et "guest" d'un RoleSchema complet :
typescript
Ici LimitedRolesSchema correspond au type Extract<Role, "user" | "guest">. En pratique, on pourrait obtenir le même résultat en définissant directement z.enum(["user","guest"]), mais .extract est pratique pour dériver un schéma d'un enum existant. Notons que .exclude et .extract de Zod s'appliquent aux schémas énumérations (z.enum ou z.nativeEnum), et non aux unions arbitraires de types complexes. Pour ces derniers, on construit généralement manuellement le nouveau schéma union désiré.
TypeScript : NonNullable<T> construit un type en excluant null et undefined de T . Il est fréquent en TypeScript d'avoir des types union incluant ces valeurs « nullish » et de vouloir un type qui en soit purgé. Par exemple :
typescript
Ici, DefinitelyString ne contient plus que string (ni null ni undefined). Ce utility type est souvent utilisé en combinaison avec des conditions de présence (if (value != null) { ... }) pour aider le compilateur à affiner les types.
Equivalent avec Zod : Avec Zod, par défaut les schémas n'acceptent pas undefined ou null à moins qu'on ne le spécifie. Autrement dit, un schéma z.string() n'acceptera qu'une string, pas undefined. Pour autoriser ces valeurs, on utilise explicitement z.string().optional() (qui permet undefined) ou z.string().nullable() (qui permet null) ou encore .nullish() (qui permet l'un ou l'autre). Ainsi, obtenir l'équivalent d'un type NonNullable revient simplement à ne pas inclure de null/undefined dans le schéma. Si toutefois vous partez d'un schéma plus permissif et voulez le rendre plus strict, vous pouvez utiliser une combinaison de techniques : par exemple, la méthode .unwrap() permet d'extraire le schéma sous-jacent d'un schéma optionnel ou nullable .
typescript
Ici, OptionalName.unwrap() renvoie le schéma original z.string(). On peut ainsi « enlever » le caractère optionnel ou nullable introduit précédemment. Une autre approche consisterait à utiliser une refinement custom pour refuser null/undefined, mais ce serait redondant étant donné le comportement par défaut de Zod. En résumé, TypeScript offre NonNullable<T> pour purifier un type au niveau statique, tandis qu'avec Zod il suffit de définir un schéma n'autorisant pas ces valeurs (ce qui est le cas par défaut, sauf usage explicite de .optional()/.nullable()).
TypeScript : Parameters<Fn> extrait le type sous forme de tuple des paramètres de la fonction Fn . Ce utility type est utile pour réutiliser les types des paramètres d'une fonction (par exemple pour typer une fonction wrapper). Exemples :
typescript
Dans ces exemples, Parameters nous donne un tuple correspondant à la liste des types des arguments de la fonction fournie. Si le type n'est pas une fonction, Parameters retourne never. Pour les fonctions surchargées, il extrait les paramètres de la dernière signature. C'est un moyen d'obtenir un type tuple [Arg0Type, Arg1Type, ...] à partir d'un type fonction.
Equivalent avec Zod : Zod permet de définir des schémas de fonctions via z.function(argsSchema, returnSchema). En plus de valider les entrées/sorties, cela offre des méthodes d'introspection telles que .parameters() qui renvoie le schéma des paramètres (sous forme de tuple) et .returnType() pour le type de retour . On peut ainsi retrouver l'information équivalente à Parameters. Par exemple :
typescript
Ici, Params est exactement le tuple des types d'arguments, équivalent à ce qu'aurait donné Parameters<(arg0: string, arg1: number) => boolean>. Certes, ce tuple pourrait être obtenu directement en TypeScript, mais Zod nous donne un moyen de l'obtenir dynamiquement si besoin, et surtout de valider que des arguments correspondent à ces types à l'exécution. Notons que dans la pratique, on utilise rarement .parameters() seul ; on pourrait directement faire type Params2 = z.infer<typeof myFunctionSchema>, qui donnerait le type de fonction complet. Cependant, .parameters() et .returnType() sont utiles si l'on veut extraire séparément le schéma des inputs ou de la sortie d'une fonction.
TypeScript : ReturnType<Fn> donne le type du résultat de la fonction Fn . C'est utile pour capturer le type qu'une fonction retourne sans le redéfinir manuellement. Par exemple :
typescript
Ici, ReturnType<typeof compute> infère { result: number; error?: string }. Comme pour Parameters, si le type fourni n'est pas une fonction, on obtient never.
Equivalent avec Zod : Poursuivant l'exemple précédent du schéma de fonction, Zod fournit .returnType() qui renvoie le schéma de sortie . Reprenons myFunctionSchema :
typescript
Ce ReturnT correspondrait au ReturnType du type de fonction décrit par myFunctionSchema. Encore une fois, dans un scénario réel, on pourrait directement utiliser z.infer<typeof myFunctionSchema> pour obtenir le type de la fonction (string, number) => boolean, puis en extraire le retour, mais .returnType() nous fournit explicitement le schéma (et donc le type) de retour. L'intérêt concret de ReturnType en TypeScript est souvent de typer une variable qui va recevoir le retour d'une fonction (sans avoir à dupliquer ce type). Dans Zod, l'analogie serait de réutiliser returnSchema pour valider qu'une valeur correspond bien à ce que la fonction devrait retourner, ou de composer ce schéma de retour avec d'autres.
TypeScript : InstanceType<C> obtient le type de l'instance créée par le constructeur C . Habituellement on l'utilise en passant une classe (via typeof MaClasse). Par exemple :
typescript
Ici, InstanceType<typeof Person> est tout simplement Person. Cela peut paraître trivial, mais devient utile si on manipule des types de classes de manière générique (par exemple, une fonction générique qui prend un constructeur en paramètre et qui veut connaître le type des instances de ce constructeur).
Equivalent avec Zod : Zod propose un schéma z.instanceof(SomeClass) pour valider qu'une valeur est bien une instance de la classe donnée . Le schéma résultant, une fois inféré, correspond au type de l'instance. Par exemple :
typescript
CarSchema vérifiera à l'exécution que la valeur est un instanceof Car. Cela va au-delà de TypeScript qui, statiquement, ne fait que lier le type de l'instance au constructeur. Avec Zod, on a une validation runtime sur la provenance de l'objet (même si celui-ci a les propriétés requises, il doit hériter de Car pour passer le test). Ainsi, on obtient une garantie plus forte sur la nature de l'objet.
Il convient de noter que InstanceType (TypeScript) et z.instanceof (Zod) répondent à des besoins différents : le premier dérive un type statique de classe, le second vérifie dynamiquement une instance. Lorsqu'on infère le type de z.instanceof(X), on retrouve bien le type X attendu.
TypeScript : ThisType<T> est un utility un peu particulier car il ne produit pas un nouveau type en soi, mais sert de marqueur contextuel pour indiquer le type de this à l'intérieur d'un objet littéral . Il s'utilise surtout dans des scénarios avancés (par exemple pour typer le this dans une usine d'objets contenant données + méthodes). Par exemple, dans le code suivant :
typescript
Dans cet exemple (inspiré de la documentation TypeScript ), ThisType<D & M> indique au compilateur que à l'intérieur de methods, le mot-clé this doit être du type intersection D & M (ici {x: number, y: number} & { moveBy(dx: number, dy: number): void }). Ainsi, this.x et this.y sont correctement reconnus comme number. ThisType n'a d'effet que si le flag --noImplicitThis est activé et uniquement dans le contexte des objets littéraux. En dehors, c'est une interface vide sans impact .
Equivalent avec Zod : Étant donné que ThisType est purement un outil du système de types pour aider à la cohérence du this à la compilation, il n'a pas d'équivalent dans Zod. Zod ne traite que de la validation de valeurs à l'exécution. Le concept de contexte this est lié à la façon dont les fonctions sont appelées en JavaScript, ce qui sort du domaine de la validation de données. Si vous avez un objet dont les méthodes utilisent this, TypeScript peut vous aider avec ThisType pour le typer, mais Zod ne peut pas valider ou influencer cela au runtime (ce serait plutôt le rôle de tests unitaires ou simplement de bien utiliser le this). En résumé, pas d'équivalent direct dans Zod pour ThisType et les utilitaires associés comme ThisParameterType ou OmitThisParameter – ces derniers servent à manipuler statiquement les signatures de fonctions qui utilisent this.
TypeScript : ConstructorParameters<C> est à new ce que Parameters est à call : il extrait sous forme de tuple les types des paramètres du constructeur C (qui doit être du type abstract new (...args) => any) . Par exemple :
typescript
Ici CircleParams est un tuple correspondant aux arguments du constructeur de Circle. Cet utility est surtout utile dans les fabriques ou lorsqu'on veut créer un type représentant les arguments requis pour instancier une classe.
Equivalent avec Zod : Zod n'a pas de méthode spécifique pour introspecter directement le constructeur d'une classe, mais on peut parvenir à un résultat similaire en combinant d'autres fonctionnalités. Par exemple, pour valider les arguments qu'on compte passer à new Circle(...), on pourrait définir un tuple schéma z.tuple([z.number(), z.string().optional()]) et l'utiliser pour vérifier un array de paramètres. Cependant, Zod n'a pas une méthode constructorParameters() dédiée. Dans la pratique, on utilisera plutôt InstanceType et Parameters avec Zod : par exemple, z.instanceof(Circle) pour valider une instance, et possiblement un schéma de tuple pour valider des arguments avant de les passer au constructeur. L'analogie directe de ConstructorParameters est donc limitée côté Zod – on pourrait dire qu'il n'y en a pas spécifiquement, puisque Zod ne modélise pas la signature des constructeurs.
En résumé, pour toutes les utilités autour de this et des signatures de fonctions (paramètre this, suppression de this, paramètres de constructeurs, etc.), TypeScript propose des solutions statiques, alors que Zod ne couvre pas ces préoccupations (sauf via des constructions manuelles) car elles relèvent plus du langage que de la validation de données externes.
Les utility types de TypeScript et les schémas Zod servent tous deux à garantir la fiabilité des données, mais à des niveaux différents : l'un au moment de la compilation, l'autre à l'exécution.
On peut résumer ainsi :
En termes de philosophie, les utility types expriment des transformations ou contraintes au niveau du système de types (ils aident le développeur, n'ayant aucun impact direct sur le code émis), alors que Zod fournit des contrats au niveau du runtime (ils permettent d'implémenter concrètement des validations et transformations de données à l'exécution). Les deux approches répondent à des besoins différents mais liés : maintenir la robustesse du code. D'ailleurs, Zod est conçu pour s'intégrer harmonieusement avec TypeScript : il infère automatiquement des types statiques de vos schémas, évitant la duplication de définitions . Cela veut dire que vous pouvez souvent définir un schéma Zod (pour la validation) et utiliser z.infer pour en tirer le type TypeScript statique correspondant, au lieu de définir une interface TypeScript et un schéma séparément – on a ainsi une source de vérité unique pour vos types de données .
En conclusion, connaître les utility types de TypeScript vous permet de tirer profit du système de types pour écrire un code sûr et expressif, tandis que maîtriser Zod vous donne les outils pour faire respecter ces contrats de types à l'exécution. Utilisés ensemble, ils renforcent considérablement la fiabilité de vos applications TypeScript : TypeScript vous protège des erreurs de programmation, et Zod protège votre application des données imprévues du monde extérieur. Créez vos types, validez vos données, et codez sereinement 🎉!