Français

Un guide complet sur les puissants Types Mappés et Types Conditionnels de TypeScript, avec des exemples pratiques et des cas d'usage avancés pour créer des applications robustes et typées.

Maîtriser les Types Mappés et les Types Conditionnels de TypeScript

TypeScript, un sur-ensemble de JavaScript, offre des fonctionnalités puissantes pour créer des applications robustes et maintenables. Parmi ces fonctionnalités, les Types Mappés (Mapped Types) et les Types Conditionnels (Conditional Types) se distinguent comme des outils essentiels pour la manipulation de types avancée. Ce guide fournit une vue d'ensemble complète de ces concepts, explorant leur syntaxe, leurs applications pratiques et leurs cas d'usage avancés. Que vous soyez un développeur TypeScript expérimenté ou que vous commenciez tout juste votre parcours, cet article vous dotera des connaissances nécessaires pour exploiter efficacement ces fonctionnalités.

Que sont les Types Mappés ?

Les Types Mappés vous permettent de créer de nouveaux types en transformant des types existants. Ils itèrent sur les propriétés d'un type existant et appliquent une transformation à chaque propriété. Ceci est particulièrement utile pour créer des variations de types existants, comme rendre toutes les propriétés optionnelles ou en lecture seule.

Syntaxe de Base

La syntaxe d'un Type Mappé est la suivante :

type NewType<T> = {
  [K in keyof T]: Transformation;
};

Exemples Pratiques

Rendre les Propriétés en Lecture Seule

Imaginons que vous ayez une interface représentant un profil utilisateur :

interface UserProfile {
  name: string;
  age: number;
  email: string;
}

Vous pouvez créer un nouveau type où toutes les propriétés sont en lecture seule :

type ReadOnlyUserProfile = {
  readonly [K in keyof UserProfile]: UserProfile[K];
};

Maintenant, ReadOnlyUserProfile aura les mêmes propriétés que UserProfile, mais elles seront toutes en lecture seule.

Rendre les Propriétés Optionnelles

De même, vous pouvez rendre toutes les propriétés optionnelles :

type OptionalUserProfile = {
  [K in keyof UserProfile]?: UserProfile[K];
};

OptionalUserProfile aura toutes les propriétés de UserProfile, mais chaque propriété sera optionnelle.

Modifier les Types des Propriétés

Vous pouvez également modifier le type de chaque propriété. Par exemple, vous pouvez transformer toutes les propriétés en chaînes de caractères :

type StringifiedUserProfile = {
  [K in keyof UserProfile]: string;
};

Dans ce cas, toutes les propriétés de StringifiedUserProfile seront de type string.

Que sont les Types Conditionnels ?

Les Types Conditionnels vous permettent de définir des types qui dépendent d'une condition. Ils fournissent un moyen d'exprimer des relations de type basées sur le fait qu'un type satisfait ou non une contrainte particulière. C'est similaire à un opérateur ternaire en JavaScript, mais pour les types.

Syntaxe de Base

La syntaxe d'un Type Conditionnel est la suivante :

T extends U ? X : Y

Exemples Pratiques

Déterminer si un Type est une Chaîne de Caractères

Créons un type qui retourne string si le type d'entrée est une chaîne de caractères, et number sinon :

type StringOrNumber<T> = T extends string ? string : number;

type Result1 = StringOrNumber<string>;  // string
type Result2 = StringOrNumber<number>;  // number
type Result3 = StringOrNumber<boolean>; // number

Extraire un Type d'une Union

Vous pouvez utiliser les types conditionnels pour extraire un type spécifique d'un type union. Par exemple, pour extraire les types non-nullables :

type NonNullable<T> = T extends null | undefined ? never : T;

type Result4 = NonNullable<string | null | undefined>; // string

Ici, si T est null ou undefined, le type devient never, qui est ensuite filtré par la simplification des types union de TypeScript.

Inférence de Types

Les types conditionnels peuvent également être utilisés pour inférer des types à l'aide du mot-clé infer. Cela vous permet d'extraire un type d'une structure de type plus complexe.

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

function myFunction(x: number): string {
  return x.toString();
}

type Result5 = ReturnType<typeof myFunction>; // string

Dans cet exemple, ReturnType extrait le type de retour d'une fonction. Il vérifie si T est une fonction qui prend n'importe quels arguments et retourne un type R. Si c'est le cas, il retourne R ; sinon, il retourne any.

Combiner les Types Mappés et les Types Conditionnels

La véritable puissance des Types Mappés et des Types Conditionnels vient de leur combinaison. Cela permet de créer des transformations de types très flexibles et expressives.

Exemple : Readonly en Profondeur

Un cas d'usage courant est de créer un type qui rend toutes les propriétés d'un objet, y compris les propriétés imbriquées, en lecture seule. Cela peut être réalisé à l'aide d'un type conditionnel récursif.

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

interface Company {
  name: string;
  address: {
    street: string;
    city: string;
  };
}

type ReadonlyCompany = DeepReadonly<Company>;

Ici, DeepReadonly applique récursivement le modificateur readonly à toutes les propriétés et à leurs propriétés imbriquées. Si une propriété est un objet, il appelle récursivement DeepReadonly sur cet objet. Sinon, il applique simplement le modificateur readonly à la propriété.

Exemple : Filtrer les Propriétés par Type

Disons que vous souhaitez créer un type qui n'inclut que les propriétés d'un type spécifique. Vous pouvez combiner les Types Mappés et les Types Conditionnels pour y parvenir.

type FilterByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface Person {
  name: string;
  age: number;
  isEmployed: boolean;
}

type StringProperties = FilterByType<Person, string>; // { name: string; }

type NonStringProperties = Omit<Person, keyof StringProperties>;

Dans cet exemple, FilterByType itère sur les propriétés de T et vérifie si le type de chaque propriété étend U. Si c'est le cas, il inclut la propriété dans le type résultant ; sinon, il l'exclut en mappant la clé sur never. Notez l'utilisation de "as" pour remapper les clés. Nous utilisons ensuite `Omit` et `keyof StringProperties` pour supprimer les propriétés de type chaîne de caractères de l'interface originale.

Cas d'Usage Avancés et Patrons

Au-delà des exemples de base, les Types Mappés et les Types Conditionnels peuvent être utilisés dans des scénarios plus avancés pour créer des applications hautement personnalisables et typées.

Types Conditionnels Distributifs

Les types conditionnels sont distributifs lorsque le type vérifié est un type union. Cela signifie que la condition est appliquée à chaque membre de l'union individuellement, et les résultats sont ensuite combinés en un nouveau type union.

type ToArray<T> = T extends any ? T[] : never;

type Result6 = ToArray<string | number>; // string[] | number[]

Dans cet exemple, ToArray est appliqué à chaque membre de l'union string | number individuellement, ce qui donne string[] | number[]. Si la condition n'était pas distributive, le résultat aurait été (string | number)[].

Utilisation des Types Utilitaires

TypeScript fournit plusieurs types utilitaires intégrés qui exploitent les Types Mappés et les Types Conditionnels. Ces types utilitaires peuvent être utilisés comme briques de base pour des transformations de types plus complexes.

Ces types utilitaires sont des outils puissants qui peuvent simplifier des manipulations de types complexes. Par exemple, vous pouvez combiner Pick et Partial pour créer un type qui ne rend que certaines propriétés optionnelles :

type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
}

type OptionalDescriptionProduct = Optional<Product, "description">;

Dans cet exemple, OptionalDescriptionProduct a toutes les propriétés de Product, mais la propriété description est optionnelle.

Utilisation des Types Littéraux de Gabarit

Les Types Littéraux de Gabarit (Template Literal Types) vous permettent de créer des types basés sur des littéraux de chaîne de caractères. Ils peuvent être utilisés en combinaison avec les Types Mappés et les Types Conditionnels pour créer des transformations de types dynamiques et expressives. Par exemple, vous pouvez créer un type qui préfixe tous les noms de propriété avec une chaîne de caractères spécifique :

type Prefix<T, P extends string> = {
  [K in keyof T as `${P}${string & K}`]: T[K];
};

interface Settings {
  apiUrl: string;
  timeout: number;
}

type PrefixedSettings = Prefix<Settings, "data_">;

Dans cet exemple, PrefixedSettings aura les propriétés data_apiUrl et data_timeout.

Meilleures Pratiques et Considérations

Conclusion

Les Types Mappés et les Types Conditionnels sont des fonctionnalités puissantes de TypeScript qui vous permettent de créer des transformations de types très flexibles et expressives. En maîtrisant ces concepts, vous pouvez améliorer la sécurité des types, la maintenabilité et la qualité globale de vos applications TypeScript. Des transformations simples comme rendre les propriétés optionnelles ou en lecture seule aux transformations récursives complexes et à la logique conditionnelle, ces fonctionnalités fournissent les outils dont vous avez besoin pour construire des applications robustes et évolutives. Continuez à explorer et à expérimenter avec ces fonctionnalités pour libérer leur plein potentiel et devenir un développeur TypeScript plus compétent.

Alors que vous poursuivez votre parcours avec TypeScript, n'oubliez pas de tirer parti de la richesse des ressources disponibles, y compris la documentation officielle de TypeScript, les communautés en ligne et les projets open-source. Adoptez la puissance des Types Mappés et des Types Conditionnels, et vous serez bien équipé pour aborder même les problèmes de typage les plus complexes.