Comprendre `infer` en TypeScript
infer déclare une variable de type à l’intérieur de la clause extends d’un type conditionnel, capturant un sous-type correspondant afin de pouvoir l’utiliser dans la branche vraie — il n’a aucune signification en dehors de cette position et constitue une erreur de compilation partout ailleurs. Si vous avez déjà ouvert le fichier .d.ts d’une bibliothèque ou la PR d’un collègue et rencontré quelque chose comme T extends (...args: any[]) => infer R ? R : any, cette ligne utilise infer pour extraire un fragment d’un type, de la même façon que la déstructuration extrait une valeur d’un objet.
Points clés à retenir
infern’est valide qu’à l’intérieur de la clauseextendsd’un type conditionnel, et la variable qu’il introduit n’est accessible que dans la branche vraie — y faire référence dans la branche fausse est une erreur de compilation.infera été introduit dans TypeScript 2.8 ; l’utilisation d’inferdans les types littéraux de gabarit est arrivée en 4.1,Awaited<T>en 4.5, etinfer X extends Constrainten 4.7.- Lorsque le paramètre de type est une variable de type nue et que vous passez une union, le conditionnel se distribue sur chaque membre, de sorte qu’
inferse résout une fois par membre et que les résultats se regroupent en une union — un bug d’élargissement silencieux fréquent. - Depuis TypeScript 4.5,
Awaited<T>est le type natif permettant de déballer les chaînes dePromise; il n’y a aucune raison de réimplémenterUnwrapPromisemanuellement. - Plusieurs candidats pour la même variable
inferproduisent une union en position covariante et une intersection en position contravariante.
Les types conditionnels : le substrat dans lequel vit infer
Un type conditionnel sélectionne l’un de deux types selon qu’un type est assignable à un autre, en utilisant la forme T extends X ? A : B. Il fonctionne comme un opérateur ternaire, mais au niveau des types : si T est assignable à X, le type se résout en A, sinon en B. Le manuel TypeScript sur les types conditionnels constitue la référence principale.
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
Le comportement qui déroute les développeurs est la distributivité. Lorsque le type vérifié est un paramètre de type nu — T directement, sans encapsulation — et que vous passez une union, TypeScript distribue le conditionnel sur chaque membre de l’union séparément et regroupe les résultats en une nouvelle union. Encapsuler le paramètre dans un tuple ([T] extends [X]) désactive la distribution et vérifie l’union comme une seule unité. Cette distinction est documentée dans la section sur les types conditionnels distributifs.
type Distributed<T> = T extends string ? T[] : never;
type R1 = Distributed<string | number>; // string[] (le membre number se résout en never et est éliminé)
type NonDistributed<T> = [T] extends [string] ? T[] : never;
type R2 = NonDistributed<string | number>; // never — vérifié comme une seule unité
Gardez cette règle en tête : c’est à l’origine de la surprise la plus fréquente avec infer, abordée dans la section sur les pièges ci-dessous.
Anatomie d’un pattern infer
Discover how at OpenReplay.com.
Une variable de type introduite avec infer n’est accessible que dans la branche vraie du type conditionnel ; y faire référence dans la branche fausse est une erreur de compilation TypeScript. infer introduit la variable à l’intérieur d’un pattern extends et la lie à ce qui correspond à cette position lors de l’évaluation du conditionnel. Cela donne à infer un comportement similaire à la déstructuration — mais pour les types : vous écrivez un pattern qui reflète la forme attendue, vous nommez les parties que vous souhaitez extraire, et TypeScript les renseigne.
type ElementType<T> = T extends (infer U)[] ? U : T;
type A = ElementType<string[]>; // string
type B = ElementType<number>; // number
Le pattern (infer U)[] signifie « un tableau d’un certain type d’élément — appelons ce type d’élément U ». Lorsque T est string[], U est lié à string. Lorsque la correspondance échoue, la branche fausse s’exécute, et U n’est plus disponible :
type Bad<T> = T extends (infer U)[] ? U[] : U;
// ^ Erreur : Impossible de trouver le nom 'U'.
Un seul pattern peut contenir plusieurs variables infer, chacune liée à sa propre position :
type SplitFn<T> = T extends (arg: infer A) => infer R ? [A, R] : never;
type S = SplitFn<(x: number) => string>; // [number, string]
Ici, infer A capture le type du paramètre et infer R capture le type de retour en une seule passe. C’est exactement la technique qu’utilisent les types utilitaires natifs.
infer dans la bibliothèque standard : les extracteurs canoniques
La bibliothèque standard TypeScript implémente ses types utilitaires d’extraction avec infer. Chacun suit la même structure : faire correspondre un pattern, lier la position intéressante, la retourner dans la branche vraie. Les définitions ci-dessous sont celles livrées dans lib/es5.d.ts.
Type de retour d’une fonction — ReturnType
ReturnType<T> extrait le type de retour d’un type fonction en faisant correspondre la forme de la fonction et en liant la position de retour à infer R. La définition de la bibliothèque standard contraint T à être appelable.
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
type A = ReturnType<() => string>; // string
type B = ReturnType<() => { id: number }>; // { id: number }
Notez la contrainte générique T extends (...args: any) => any — elle fait partie de la définition réelle de la bibliothèque standard, et non d’une décoration facultative.
À retenir : ReturnType est l’exemple le plus clair d’utilisation d’infer — faire correspondre la fonction par pattern-matching et capturer l’emplacement du type de retour.
Paramètres d’une fonction sous forme de tuple — Parameters
Parameters<T> extrait la liste des paramètres d’une fonction sous forme de tuple en liant la position du paramètre rest à infer P.
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
type Args = Parameters<(a: string, b: number) => void>; // [a: string, b: number]
À retenir : Puisque les paramètres sont modélisés comme un type tuple, infer P sur ...args vous donne la liste complète, en préservant les étiquettes et l’optionnalité.
Résolution de Promise — utilisez Awaited<T>, pas un déballeur artisanal
Depuis TypeScript 4.5, le type natif Awaited<T> déroule récursivement les chaînes de Promise et gère les PromiseLike — il n’y a aucune raison de réimplémenter UnwrapPromise manuellement dans un TypeScript moderne. De nombreux tutoriels plus anciens définissent leur propre T extends Promise<infer U> ? U : T, mais cette version à un seul niveau ne déroule pas les promises imbriquées ni ne modélise les objets .then-able. Awaited fait les deux, comme décrit dans les notes de version de TypeScript 4.5.
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number — récursif
type C = Awaited<boolean | Promise<number>>; // number | boolean
À retenir : Utilisez directement Awaited<T>. N’écrivez votre propre déballeur de promise basé sur infer qu’à titre d’exercice d’apprentissage.
Type d’élément d’un tableau
Extraire le type d’élément d’un tableau est le plus petit pattern infer utile : faire correspondre (infer U)[] et retourner U.
type ElementOf<T> = T extends readonly (infer U)[] ? U : never;
type A = ElementOf<number[]>; // number
type B = ElementOf<readonly string[]>; // string
À retenir : Incluez readonly dans le pattern afin que le type utilitaire fonctionne aussi bien pour les tableaux mutables que pour les tableaux en lecture seule.
Tête, queue et reste d’un tuple
Les tuples prennent en charge les patterns infer positionnels avec des éléments rest, vous permettant d’extraire la tête, la queue ou le dernier élément. Les patterns reflètent la déstructuration de tableaux en JavaScript.
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type Tail<T extends any[]> = T extends [any, ...infer Rest] ? Rest : never;
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type H = Head<[1, 2, 3]>; // 1
type R = Tail<[1, 2, 3]>; // [2, 3]
type L = Last<[1, 2, 3]>; // 3
À retenir : [infer Head, ...infer Rest] et [...any[], infer Last] sont les briques de base pour la manipulation récursive de tuples — les mêmes patterns alimentent les routeurs au niveau des types et les bibliothèques de gestion d’état de formulaires.
Inférence récursive — Flatten
infer combiné à la récursion permet à un type de décortiquer des structures arbitrairement profondes. Flatten récurse à travers des tableaux imbriqués jusqu’à atteindre un type non-tableau.
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;
type A = Flatten<number[][][]>; // number
type B = Flatten<string>; // string
Chaque passe lie U au type d’élément et le réinjecte dans Flatten. Lorsque T n’est plus un tableau, la branche fausse le retourne tel quel.
À retenir : L’infer récursif est la façon de réduire des conteneurs imbriqués ; la même structure déroule des types Promise ou des types d’objets profondément imbriqués.
Inférence dans les types littéraux de gabarit — analyser des chaînes au niveau des types
Les types littéraux de gabarit prennent en charge infer à l’intérieur de leurs positions de substitution. L’exemple ExtractId<T> ci-dessous fait correspondre le pattern de chaîne et lie le segment variable à Id, permettant l’extraction typée de paramètres de route sans analyseur syntaxique à l’exécution — un pattern disponible depuis TypeScript 4.1.
type ExtractId<T> = T extends `/users/${infer Id}` ? Id : never;
type A = ExtractId<"/users/42">; // "42"
type B = ExtractId<"/posts/42">; // never
Vous pouvez enchaîner infer sur plusieurs segments pour analyser un chemin complet en ses différentes parties :
type RouteParams<T> =
T extends `${infer _Start}/:${infer Param}/${infer Rest}`
? Param | RouteParams<`/${Rest}`>
: T extends `${infer _Start}/:${infer Param}`
? Param
: never;
type P = RouteParams<"/users/:userId/posts/:postId">; // "userId" | "postId"
C’est l’équivalent au niveau des types d’un analyseur de chemin, et c’est le fondement du routage typé dans les frameworks et bibliothèques qui dérivent des objets de paramètres à partir de chaînes de route.
À retenir : L’infer dans les types littéraux de gabarit transforme des types en forme de chaîne en données structurées sans aucun coût à l’exécution — utile pour les paramètres de route, les unions de propriétés CSS et l’analyse de formats de chaînes de marque.
Inférence contrainte : infer X extends Constraint (TS 4.7)
TypeScript 4.7 a ajouté infer X extends Constraint, permettant à un type conditionnel de contraindre une variable inférée au moment de la correspondance — combinant inférence et vérification de contrainte en une seule étape. Cela est documenté dans les notes de version de TypeScript 4.7. Avant la version 4.7, il fallait d’abord inférer, puis ajouter un second conditionnel imbriqué pour affiner le résultat.
// Avant 4.7 : inférer, puis vérifier dans un second conditionnel
type FirstStringOld<T> =
T extends [infer H, ...any[]]
? H extends string ? H : never
: never;
// 4.7+ : contraindre au moment de l'inférence
type FirstString<T> =
T extends [infer H extends string, ...any[]] ? H : never;
type A = FirstString<["hi", 1, 2]>; // "hi"
type B = FirstString<[1, 2, 3]>; // never
TypeScript 4.8 a ensuite amélioré l’inférence contrainte pour les types primitifs tels que number, bigint et boolean, permettant au compilateur de préserver des types littéraux plus précis lors de la correspondance de patterns de chaînes de gabarit. Cela s’appuie sur la syntaxe infer contrainte introduite dans TypeScript 4.7, mais a été livré comme une amélioration distincte.
À retenir : Utilisez infer X extends C lorsque vous souhaitez à la fois le type capturé et une garantie sur sa forme en une seule expression — cela remplace l’ancien pattern inférer-puis-imbriquer.
Trois pièges qui brisent silencieusement l’inférence
Les trois pièges qui brisent silencieusement infer sont : la distributivité sur les unions qui élargit les résultats, le retournement de variance qui fait basculer plusieurs candidats infer entre union et intersection, et les patterns trop spécifiques qui tombent dans la branche fausse. Les échecs d’infer sont généralement silencieux — le type compile toujours, mais il est simplement plus large ou plus étroit que prévu.
1. La distributivité élargit silencieusement les résultats
Lorsque le paramètre de type T est une variable de type nue et que vous passez une union, TypeScript distribue le conditionnel sur chaque membre séparément — ce qui signifie qu’infer R se résout une fois par membre de l’union et que les résultats sont regroupés en une union, ce qui peut silencieusement élargir le type inféré au-delà de ce que vous souhaitiez.
type Unwrap<T> = T extends Promise<infer U> ? U : T;
// Semble retourner un seul type ; se distribue en réalité
type R = Unwrap<Promise<string> | Promise<number>>; // string | number
Si vous vouliez rejeter les unions mixtes plutôt que de les réduire, encapsulez le paramètre pour désactiver la distribution :
type UnwrapStrict<T> = [T] extends [Promise<infer U>] ? U : T;
À retenir : Un T nu combiné à une union signifie une évaluation par membre. Encapsulez dans [T] lorsque vous avez besoin que l’union soit traitée comme une seule unité.
2. La position covariante ou contravariante change le résultat
Plusieurs candidats pour la même variable infer produisent une union en positions covariantes (par ex., les types de retour) et une intersection en positions contravariantes (par ex., les types de paramètres de fonction). Pour les fonctions surchargées, l’inférence utilise la dernière signature. Ce comportement est décrit dans les notes de version de TypeScript 2.8.
// Covariant (position de retour) : union
type Co<T> = T extends { a: () => infer U; b: () => infer U } ? U : never;
type C = Co<{ a: () => string; b: () => number }>; // string | number
// Contravariant (position de paramètre) : intersection
type Contra<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never;
type D = Contra<{ a: (x: string) => void; b: (x: number) => void }>; // string & number
À retenir : Réutiliser un même nom infer dans plusieurs positions est intentionnel et puissant, mais le fait d’obtenir une union ou une intersection dépend de la variance des positions. Ne supposez pas systématiquement une union.
3. Les patterns qui ne peuvent pas se lier tombent dans la branche fausse
infer ne se lie que lorsque le type candidat correspond structurellement au pattern. Si la forme ne correspond pas, le conditionnel prend la branche fausse et la variable inférée ne se résout jamais — ainsi, un pattern trop spécifique retourne silencieusement votre type de repli au lieu de la valeur attendue.
type FirstArg<T> = T extends (a: infer A, b: infer B) => any ? A : never;
type X = FirstArg<string>; // never — string ne correspond pas à un type fonction
Dans cet exemple, FirstArg attend un type fonction. Passer string ne correspond pas à ce pattern, donc le conditionnel se résout dans la branche fausse et retourne never.
À retenir : Rendez les patterns aussi souples que la correspondance le permet. Utilisez ...args: infer P ou [infer H, ...any[]] plutôt que des formes à arité fixe, sauf si vous souhaitez spécifiquement rejeter d’autres arités.
Où vous rencontrez réellement infer en pratique
Dans le code applicatif, vous écrivez rarement infer vous-même — vous le rencontrez dans les définitions de types de bibliothèques comme ComponentProps de React, RequestHandler d’Express, PayloadAction de Redux Toolkit, et les types d’endpoints de RTK Query, et vous utilisez les types utilitaires qui en résultent. Reconnaître le pattern T extends Wrapper<infer X> ? X : Fallback vous permet de lire ces définitions au lieu de les traiter comme des boîtes noires.
ComponentProps de React. Les typages de @types/react définissent ComponentProps avec infer pour extraire les props d’un composant à partir de son type, en distinguant les éléments hôtes des constructeurs de composants. En le lisant, vous pouvez voir la même structure T extends ... infer P ... ? P : ... que dans ReturnType, appliquée aux types d’éléments React.
Les gestionnaires de requêtes Express. Le module @types/express d’Express modélise RequestHandler comme un générique avec des paramètres pour les paramètres de route, le corps de la réponse, le corps de la requête et la requête. Les bibliothèques qui dérivent des gestionnaires de route typés à partir d’une chaîne de chemin combinent ces génériques avec l’infer des types littéraux de gabarit pour extraire les segments :param — le pattern RouteParams présenté précédemment est le cœur de ce mécanisme.
PayloadAction de Redux Toolkit. PayloadAction de Redux Toolkit est un type utile à extraire avec infer, même si sa propre définition s’appuie sur des génériques et des types conditionnels plutôt que sur infer en interne — un rappel qu’infer est la façon dont vous lisez les types d’une bibliothèque, pas nécessairement la façon dont la bibliothèque est écrite. Vous écrirez couramment type Payload<A> = A extends PayloadAction<infer P> ? P : never pour récupérer le type de payload d’un réducteur à partir de son action.
import type { PayloadAction } from "@reduxjs/toolkit";
type PayloadOf<A> = A extends PayloadAction<infer P> ? P : never;
type P = PayloadOf<PayloadAction<{ id: string }>>; // { id: string }
Les endpoints de RTK Query. RTK Query (qui fait partie de Redux Toolkit) expose des types d’endpoints générés dont les types de requête et de résultat sont récupérables avec des types utilitaires basés sur infer. Extraire le type de résultat d’un endpoint avec un conditionnel qui lie la position du résultat est une tâche courante dès lors que vous reconnaissez le pattern.
Conclusion
infer est une fonctionnalité avec une seule règle : il nomme une position à l’intérieur d’un pattern de type conditionnel et lie ce qui y correspond, utilisable uniquement dans la branche vraie. Une fois que vous percevez T extends Pattern<infer X> ? X : Fallback comme une déstructuration au niveau des types, chaque type utilitaire natif et chaque typage de bibliothèque se lit comme une variation de ce même mécanisme. La prochaine fois que vous rencontrerez infer dans un fichier .d.ts, tracez le pattern : trouvez la position dans laquelle se trouve la variable, vérifiez si le paramètre est nu ou encapsulé, et confirmez que vous lisez la branche vraie. Commencez par remplacer tout UnwrapPromise artisanal dans votre base de code par Awaited<T>, et adoptez infer X extends Constraint là où vous imbriquez actuellement un second conditionnel pour affiner un résultat.
FAQ
infer extrait un type d'un type existant par pattern-matching à l'intérieur d'un conditionnel, tandis qu'un type mappé transforme chaque propriété d'un type en une nouvelle forme. Utilisez infer lorsque vous avez besoin de lire un sous-type à partir d'une structure, comme le type de retour d'une fonction ou le type d'élément d'un tableau. Utilisez un type mappé lorsque vous devez itérer sur des clés et produire un nouveau type d'objet. Ils résolvent des problèmes opposés : infer lit, les types mappés réécrivent.
Non. Une variable de type introduite avec infer n'est accessible que dans la branche vraie du type conditionnel ; y faire référence dans la branche fausse produit une erreur de compilation TypeScript telle que 'Impossible de trouver le nom'. La variable n'existe qu'une fois qu'une correspondance réussit, donc la branche fausse n'a aucune valeur à lier. Si vous avez besoin d'une valeur dans les deux cas, inférez dans la branche vraie et fournissez un type de repli indépendant dans la branche fausse.
À cause de la distributivité. Lorsque le paramètre de type est une variable de type nue et que vous passez une union, TypeScript distribue le conditionnel sur chaque membre séparément, de sorte qu'infer se résout une fois par membre de l'union et que les résultats se regroupent en une nouvelle union. Cela peut silencieusement élargir le résultat au-delà de ce que vous souhaitiez. Pour désactiver la distribution et vérifier l'union comme une seule unité, encapsulez le paramètre dans un tuple, comme dans [T] extends [Promise<infer U>] ? U : T.
Non, pas dans un TypeScript moderne. Depuis TypeScript 4.5, le type natif Awaited<T> déroule récursivement les chaînes de Promise et gère les objets then-able PromiseLike. Un T extends Promise<infer U> ? U : T artisanal ne déroule qu'un seul niveau et ignore les objets then-able, ce qui le fait échouer sur les promises imbriquées. Utilisez directement Awaited<T> dans le code applicatif, et n'écrivez votre propre déballeur de promise basé sur infer qu'à titre d'exercice d'apprentissage.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.