Un guide complet du mot-clé 'infer' en TypeScript pour l'extraction et la manipulation de types avancées avec les types conditionnels.
Maîtriser "infer" en TypeScript : Extraction de Types Conditionnels pour une Manipulation Avancée
Le système de types de TypeScript est incroyablement puissant, permettant aux développeurs de créer des applications robustes et maintenables. L'une des fonctionnalités clés qui permet cette puissance est le mot-clé infer
utilisé conjointement avec les types conditionnels. Cette combinaison fournit un mécanisme pour extraire des types spécifiques de structures de types complexes. Ce billet de blog explore en profondeur le mot-clé infer
, en expliquant sa fonctionnalité et en présentant des cas d'utilisation avancés. Nous examinerons des exemples pratiques applicables à divers scénarios de développement logiciel, de l'interaction avec les API à la manipulation complexe de structures de données.
Que sont les Types Conditionnels ?
Avant de plonger dans infer
, révisons rapidement les types conditionnels. Les types conditionnels en TypeScript vous permettent de définir un type basé sur une condition, similaire à un opérateur ternaire en JavaScript. La syntaxe de base est :
T extends U ? X : Y
Cela se lit comme : "Si le type T
est assignable au type U
, alors le type est X
; sinon, le type est Y
."
Exemple :
type IsString<T> = T extends string ? true : false;
type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false
Introduction au Mot-clé infer
Le mot-clé infer
est utilisé dans la clause extends
d'un type conditionnel pour déclarer une variable de type qui peut être déduite du type vérifié. Essentiellement, il vous permet de "capturer" une partie d'un type pour une utilisation ultérieure.
Syntaxe de base :
type MyType<T> = T extends (infer U) ? U : never;
Dans cet exemple, si T
est assignable à un certain type, TypeScript tentera de déduire le type de U
. Si l'inférence réussit, le type sera U
; sinon, il sera never
.
Exemples Simples d'infer
1. Déduire le Type de Retour d'une Fonction
Un cas d'utilisation courant est la déduction du type de retour d'une fonction :
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnType = ReturnType<typeof add>; // type AddReturnType = number
function greet(name: string): string {
return `Hello, ${name}!`;
}
type GreetReturnType = ReturnType<typeof greet>; // type GreetReturnType = string
Dans cet exemple, ReturnType<T>
prend un type de fonction T
en entrée. Il vérifie si T
est assignable à une fonction qui accepte n'importe quels arguments et retourne une valeur. Si c'est le cas, il déduit le type de retour comme R
et le retourne. Sinon, il retourne any
.
2. Déduire le Type d'Élément d'un Tableau
Un autre scénario utile est l'extraction du type d'élément d'un tableau :
type ArrayElementType<T> = T extends (infer U)[] ? U : never;
type NumberArrayType = ArrayElementType<number[]>; // type NumberArrayType = number
type StringArrayType = ArrayElementType<string[]>; // type StringArrayType = string
type MixedArrayType = ArrayElementType<(string | number)[]>; // type MixedArrayType = string | number
type NotAnArrayType = ArrayElementType<number>; // type NotAnArrayType = never
Ici, ArrayElementType<T>
vérifie si T
est un type de tableau. Si c'est le cas, il déduit le type d'élément comme U
et le retourne. Sinon, il retourne never
.
Cas d'Utilisation Avancés d'infer
1. Déduire les Paramètres d'un Constructeur
Vous pouvez utiliser infer
pour extraire les types de paramètres d'une fonction constructeur :
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
class Person {
constructor(public name: string, public age: number) {}
}
type PersonConstructorParams = ConstructorParameters<typeof Person>; // type PersonConstructorParams = [string, number]
class Point {
constructor(public x: number, public y: number) {}
}
type PointConstructorParams = ConstructorParameters<typeof Point>; // type PointConstructorParams = [number, number]
Dans ce cas, ConstructorParameters<T>
prend un type de fonction constructeur T
. Il déduit les types des paramètres du constructeur comme P
et les retourne sous forme de tuple.
2. Extraire les Propriétés des Types d'Objets
infer
peut également être utilisé pour extraire des propriétés spécifiques des types d'objets en utilisant les types mappés et les types conditionnels :
type PickByType<T, K extends keyof T, U> = {
[P in K as T[P] extends U ? P : never]: T[P];
};
interface User {
id: number;
name: string;
age: number;
email: string;
isActive: boolean;
}
type StringProperties = PickByType<User, keyof User, string>; // type StringProperties = { name: string; email: string; }
type NumberProperties = PickByType<User, keyof User, number>; // type NumberProperties = { id: number; age: number; }
//Une interface représentant des coordonnées géographiques.
interface GeoCoordinates {
latitude: number;
longitude: number;
altitude: number;
country: string;
city: string;
timezone: string;
}
type NumberCoordinateProperties = PickByType<GeoCoordinates, keyof GeoCoordinates, number>; // type NumberCoordinateProperties = { latitude: number; longitude: number; altitude: number; }
Ici, PickByType<T, K, U>
crée un nouveau type qui n'inclut que les propriétés de T
(avec des clés dans K
) dont les valeurs sont assignables au type U
. Le type mappé itère sur les clés de T
, et le type conditionnel filtre les clés qui ne correspondent pas au type spécifié.
3. Travailler avec les Promesses
Vous pouvez déduire le type résolu d'une Promise
:
type Awaited<T> = T extends Promise<infer U> ? U : T;
async function fetchData(): Promise<string> {
return 'Data from API';
}
type FetchDataType = Awaited<ReturnType<typeof fetchData>>; // type FetchDataType = string
async function fetchNumbers(): Promise<number[]> {
return [1, 2, 3];
}
type FetchedNumbersType = Awaited<ReturnType<typeof fetchNumbers>>; //type FetchedNumbersType = number[]
Le type Awaited<T>
prend un type T
, qui est censé être une Promise. Le type déduit ensuite le type résolu U
de la Promise et le retourne. Si T
n'est pas une promise, il retourne T. Il s'agit d'un type utilitaire intégré dans les versions plus récentes de TypeScript.
4. Extraire le Type d'un Tableau de Promesses
La combinaison d'Awaited
et de l'inférence de type de tableau permet d'inférer le type résolu par un tableau de Promesses. Ceci est particulièrement utile lorsqu'on travaille avec Promise.all
.
type PromiseArrayReturnType<T extends Promise<any>[]> = {
[K in keyof T]: Awaited<T[K]>;
};
async function getUSDRate(): Promise<number> {
return 0.0069;
}
async function getEURRate(): Promise<number> {
return 0.0064;
}
const rates = [getUSDRate(), getEURRate()];
type RatesType = PromiseArrayReturnType<typeof rates>;
// type RatesType = [number, number]
Cet exemple définit d'abord deux fonctions asynchrones, getUSDRate
et getEURRate
, qui simulent la récupération des taux de change. Le type utilitaire PromiseArrayReturnType
extrait ensuite le type résolu de chaque Promise
du tableau, résultant en un type tuple où chaque élément est le type attendu de la Promise
correspondante.
Exemples Pratiques dans Différents Domaines
1. Application de Commerce Électronique
Considérez une application de commerce électronique où vous récupérez les détails des produits à partir d'une API. Vous pouvez utiliser infer
pour extraire le type des données du produit :
interface Product {
id: number;
name: string;
price: number;
description: string;
imageUrl: string;
category: string;
rating: number;
countryOfOrigin: string;
}
async function fetchProduct(productId: number): Promise<Product> {
// Simuler l'appel API
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: productId,
name: 'Example Product',
price: 29.99,
description: 'A sample product',
imageUrl: 'https://example.com/image.jpg',
category: 'Electronics',
rating: 4.5,
countryOfOrigin: 'Canada'
});
}, 500);
});
}
type ProductType = Awaited<ReturnType<typeof fetchProduct>>; // type ProductType = Product
function displayProductDetails(product: ProductType) {
console.log(`Nom du produit : ${product.name}`);
console.log(`Prix : ${product.price} ${product.countryOfOrigin === 'Canada' ? 'CAD' : (product.countryOfOrigin === 'USA' ? 'USD' : 'EUR')}`);
}
fetchProduct(123).then(displayProductDetails);
Dans cet exemple, nous définissons une interface Product
et une fonction fetchProduct
qui récupère les détails du produit à partir d'une API. Nous utilisons Awaited
et ReturnType
pour extraire le type Product
du type de retour de la fonction fetchProduct
, ce qui nous permet de vérifier le typage de la fonction displayProductDetails
.
2. Internationalisation (i18n)
Supposons que vous ayez une fonction de traduction qui retourne différentes chaînes de caractères en fonction de la locale. Vous pouvez utiliser infer
pour extraire le type de retour de cette fonction pour la sécurité des types :
interface Translations {
greeting: string;
farewell: string;
welcomeMessage: (name: string) => string;
}
const enTranslations: Translations = {
greeting: 'Hello',
farewell: 'Goodbye',
welcomeMessage: (name: string) => `Welcome, ${name}!`,
};
const frTranslations: Translations = {
greeting: 'Bonjour',
farewell: 'Au revoir',
welcomeMessage: (name: string) => `Bienvenue, ${name}!`,
};
function getTranslation(locale: 'en' | 'fr'): Translations {
return locale === 'en' ? enTranslations : frTranslations;
}
type TranslationType = ReturnType<typeof getTranslation>;
function greetUser(locale: 'en' | 'fr', name: string) {
const translations = getTranslation(locale);
console.log(translations.welcomeMessage(name));
}
greetUser('fr', 'Jean'); // Output: Bienvenue, Jean!
Ici, TranslationType
est déduit comme étant l'interface Translations
, garantissant que la fonction greetUser
dispose des informations de type correctes pour accéder aux chaînes traduites.
3. Gestion des Réponses d'API
Lorsque vous travaillez avec des API, la structure de la réponse peut être complexe. infer
peut aider à extraire des types de données spécifiques des réponses d'API imbriquées :
interface ApiResponse<T> {
status: number;
data: T;
message?: string;
}
interface UserData {
id: number;
username: string;
email: string;
profile: {
firstName: string;
lastName: string;
country: string;
language: string;
}
}
async function fetchUser(userId: number): Promise<ApiResponse<UserData>> {
// Simuler l'appel API
return new Promise((resolve) => {
setTimeout(() => {
resolve({
status: 200,
data: {
id: userId,
username: 'johndoe',
email: 'john.doe@example.com',
profile: {
firstName: 'John',
lastName: 'Doe',
country: 'USA',
language: 'en'
}
}
});
}, 500);
});
}
type UserApiResponse = Awaited<ReturnType<typeof fetchUser>>;
type UserProfileType = UserApiResponse['data']['profile'];
function displayUserProfile(profile: UserProfileType) {
console.log(`Nom : ${profile.firstName} ${profile.lastName}`);
console.log(`Pays : ${profile.country}`);
}
fetchUser(123).then((response) => {
if (response.status === 200) {
displayUserProfile(response.data.profile);
}
});
Dans cet exemple, nous définissons une interface ApiResponse
et une interface UserData
. Nous utilisons infer
et l'indexation de type pour extraire le UserProfileType
de la réponse de l'API, garantissant que la fonction displayUserProfile
reçoit le type correct.
Bonnes Pratiques pour Utiliser infer
- Garder la Simplicité : Utilisez
infer
uniquement lorsque c'est nécessaire. Une utilisation excessive peut rendre votre code plus difficile à lire et à comprendre. - Documenter Vos Types : Ajoutez des commentaires pour expliquer ce que font vos types conditionnels et vos instructions
infer
. - Tester Vos Types : Utilisez la vérification de type de TypeScript pour vous assurer que vos types se comportent comme prévu.
- Considérer la Performance : Les types conditionnels complexes peuvent parfois affecter le temps de compilation. Soyez conscient de la complexité de vos types.
- Utiliser les Types Utilitaires : TypeScript fournit plusieurs types utilitaires intégrés (par exemple,
ReturnType
,Awaited
) qui peuvent simplifier votre code et réduire le besoin d'instructionsinfer
personnalisées.
Pièges Courants
- Inférence Incorrecte : Parfois, TypeScript peut déduire un type qui n'est pas celui que vous attendez. Vérifiez attentivement vos définitions de type et vos conditions.
- Dépendances Circulaires : Faites attention lors de la définition de types récursifs à l'aide d'
infer
, car ils peuvent entraîner des dépendances circulaires et des erreurs de compilation. - Types Trop Complexes : Évitez de créer des types conditionnels trop complexes qui sont difficiles à comprendre et à maintenir. Décomposez-les en types plus petits et plus gérables.
Alternatives à infer
Bien que infer
soit un outil puissant, il existe des situations où des approches alternatives peuvent être plus appropriées :
- Assertions de Type : Dans certains cas, vous pouvez utiliser des assertions de type pour spécifier explicitement le type d'une valeur au lieu de le déduire. Cependant, soyez prudent avec les assertions de type, car elles peuvent contourner la vérification de type.
- Gardiens de Type (Type Guards) : Les gardiens de type peuvent être utilisés pour affiner le type d'une valeur en fonction de vérifications d'exécution. Ceci est utile lorsque vous devez gérer différents types en fonction de conditions d'exécution.
- Types Utilitaires : TypeScript fournit un ensemble riche de types utilitaires qui peuvent gérer de nombreuses tâches de manipulation de types courantes sans avoir besoin d'instructions
infer
personnalisées.
Conclusion
Le mot-clé infer
en TypeScript, lorsqu'il est combiné avec les types conditionnels, débloque des capacités avancées de manipulation de types. Il vous permet d'extraire des types spécifiques de structures de types complexes, vous permettant d'écrire du code plus robuste, maintenable et sûr en termes de types. De la déduction des types de retour de fonction à l'extraction de propriétés des types d'objets, les possibilités sont vastes. En comprenant les principes et les bonnes pratiques décrits dans ce guide, vous pouvez exploiter infer
à son plein potentiel et élever vos compétences en TypeScript. N'oubliez pas de documenter vos types, de les tester minutieusement et de considérer les approches alternatives lorsque cela est approprié. Maîtriser infer
vous permet d'écrire du code TypeScript véritablement expressif et puissant, conduisant finalement à un meilleur logiciel.