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;
};
T
: Le type d'entrée sur lequel vous voulez mapper.K in keyof T
: Itère sur chaque clé du type d'entréeT
.keyof T
crée une union de tous les noms de propriétés deT
, etK
représente chaque clé individuelle pendant l'itération.Transformation
: La transformation que vous souhaitez appliquer à chaque propriété. Cela peut être l'ajout d'un modificateur (commereadonly
ou?
), le changement du type, ou tout autre chose.
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
T
: Le type vérifié.U
: Le type queT
doit étendre (la condition).X
: Le type à retourner siT
étendU
(la condition est vraie).Y
: Le type à retourner siT
n'étend pasU
(la condition est fausse).
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.
Partial<T>
: Rend toutes les propriétés deT
optionnelles.Required<T>
: Rend toutes les propriétés deT
requises.Readonly<T>
: Rend toutes les propriétés deT
en lecture seule.Pick<T, K>
: Sélectionne un ensemble de propriétésK
à partir deT
.Omit<T, K>
: Supprime un ensemble de propriétésK
deT
.Record<K, T>
: Construit un type avec un ensemble de propriétésK
de typeT
.Exclude<T, U>
: Exclut deT
tous les types qui sont assignables àU
.Extract<T, U>
: Extrait deT
tous les types qui sont assignables àU
.NonNullable<T>
: Exclutnull
etundefined
deT
.Parameters<T>
: Obtient les paramètres d'un type de fonctionT
.ReturnType<T>
: Obtient le type de retour d'un type de fonctionT
.InstanceType<T>
: Obtient le type d'instance d'un type de fonction constructeurT
.
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
- Restez Simple : Bien que les Types Mappés et les Types Conditionnels soient puissants, ils peuvent aussi complexifier votre code. Essayez de garder vos transformations de types aussi simples que possible.
- Utilisez les Types Utilitaires : Tirez parti des types utilitaires intégrés de TypeScript chaque fois que possible. Ils sont bien testés et peuvent simplifier votre code.
- Documentez Vos Types : Documentez clairement vos transformations de types, surtout si elles sont complexes. Cela aidera les autres développeurs à comprendre votre code.
- Testez Vos Types : Utilisez la vérification de type de TypeScript pour vous assurer que vos transformations fonctionnent comme prévu. Vous pouvez écrire des tests unitaires pour vérifier le comportement de vos types.
- Considérez la Performance : Des transformations de types complexes peuvent impacter les performances de votre compilateur TypeScript. Soyez conscient de la complexité de vos types et évitez les calculs inutiles.
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.