Français

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

Pièges Courants

Alternatives à infer

Bien que infer soit un outil puissant, il existe des situations où des approches alternatives peuvent être plus approprié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.