Explorez la technique de marquage nominal de TypeScript pour créer des types opaques, améliorer la sécurité des types et prévenir les substitutions de types involontaires. Apprenez l'implémentation pratique et les cas d'utilisation avancés.
Marques Nominales TypeScript : Définitions de Types Opaques pour une Sécurité de Type Améliorée
TypeScript, tout en offrant un typage statique, utilise principalement le typage structurel. Cela signifie que les types sont considérés comme compatibles s'ils ont la même forme, indépendamment de leurs noms déclarés. Bien que flexible, cela peut parfois entraîner des substitutions de types involontaires et une sécurité de type réduite. Le marquage nominal, également connu sous le nom de définitions de types opaques, offre un moyen d'obtenir un système de types plus robuste, plus proche du typage nominal, au sein de TypeScript. Cette approche utilise des techniques astucieuses pour faire en sorte que les types se comportent comme s'ils étaient nommés de manière unique, empêchant les erreurs de mélange accidentelles et garantissant la correction du code.
Comprendre le Typage Structurel vs. Nominal
Avant de plonger dans le marquage nominal, il est crucial de comprendre la différence entre le typage structurel et le typage nominal.
Typage Structurel
En typage structurel, deux types sont considérés comme compatibles s'ils ont la même structure (c'est-à -dire les mêmes propriétés avec les mêmes types). Considérez cet exemple TypeScript :
interface Kilogram {
value: number;
}
interface Gram {
value: number;
}
const kg: Kilogram = { value: 10 };
const g: Gram = { value: 10000 };
// TypeScript autorise ceci car les deux types ont la mĂŞme structure
const kg2: Kilogram = g;
console.log(kg2);
Même si `Kilogram` et `Gram` représentent différentes unités de mesure, TypeScript autorise l'affectation d'un objet `Gram` à une variable `Kilogram` car ils ont tous deux une propriété `value` de type `number`. Cela peut entraîner des erreurs logiques dans votre code.
Typage Nominal
En revanche, le typage nominal considère deux types comme compatibles uniquement s'ils portent le même nom ou si l'un est explicitement dérivé de l'autre. Des langages comme Java et C# utilisent principalement le typage nominal. Si TypeScript utilisait le typage nominal, l'exemple ci-dessus générerait une erreur de type.
Le Besoin de Marquage Nominal dans TypeScript
Le typage structurel de TypeScript est généralement bénéfique pour sa flexibilité et sa facilité d'utilisation. Cependant, il existe des situations où vous avez besoin d'un contrôle de type plus strict pour prévenir les erreurs logiques. Le marquage nominal fournit une solution de contournement pour obtenir ce contrôle plus strict sans sacrifier les avantages de TypeScript.
Considérez ces scénarios :
- Gestion des devises : Différencier les montants en `USD` et en `EUR` pour éviter les mélanges accidentels de devises.
- Identifiants de base de données : S'assurer qu'un `UserID` n'est pas accidentellement utilisé là où un `ProductID` est attendu.
- Unités de mesure : Différencier les `Mètres` et les `Pieds` pour éviter les calculs incorrects.
- Données sécurisées : Différencier le `Password` en texte brut du `PasswordHash` haché pour éviter d'exposer accidentellement des informations sensibles.
Dans chacun de ces cas, le typage structurel peut entraîner des erreurs car la représentation sous-jacente (par exemple, un nombre ou une chaîne) est la même pour les deux types. Le marquage nominal vous aide à appliquer la sécurité des types en rendant ces types distincts.
Implémentation des Marques Nominales dans TypeScript
Il existe plusieurs façons d'implémenter le marquage nominal dans TypeScript. Nous allons explorer une technique courante et efficace utilisant des intersections et des symboles uniques.
Utilisation d'Intersections et de Symboles Uniques
Cette technique implique la création d'un symbole unique et son intersection avec le type de base. Le symbole unique agit comme une "marque" qui distingue le type des autres ayant la même structure.
// Définir un symbole unique pour la marque Kilogram
const kilogramBrand: unique symbol = Symbol();
// Définir un type Kilogram marqué avec le symbole unique
type Kilogram = number & { readonly [kilogramBrand]: true };
// Définir un symbole unique pour la marque Gram
const gramBrand: unique symbol = Symbol();
// Définir un type Gram marqué avec le symbole unique
type Gram = number & { readonly [gramBrand]: true };
// Fonction d'aide pour créer des valeurs Kilogram
const Kilogram = (value: number) => value as Kilogram;
// Fonction d'aide pour créer des valeurs Gram
const Gram = (value: number) => value as Gram;
const kg: Kilogram = Kilogram(10);
const g: Gram = Gram(10000);
// Ceci provoquera maintenant une erreur TypeScript
// const kg2: Kilogram = g; // Type 'Gram' is not assignable to type 'Kilogram'.
console.log(kg, g);
Explication :
- Nous définissons un symbole unique en utilisant `Symbol()`. Chaque appel à `Symbol()` crée une valeur unique, garantissant que nos marques sont distinctes.
- Nous définissons les types `Kilogram` et `Gram` comme des intersections de `number` et d'un objet contenant le symbole unique comme clé avec une valeur `true`. Le modificateur `readonly` garantit que la marque ne peut pas être modifiée après sa création.
- Nous utilisons des fonctions d'aide (`Kilogram` et `Gram`) avec des assertions de type (`as Kilogram` et `as Gram`) pour créer des valeurs des types marqués. Ceci est nécessaire car TypeScript ne peut pas inférer automatiquement le type marqué.
Désormais, TypeScript signale correctement une erreur lorsque vous essayez d'affecter une valeur `Gram` à une variable `Kilogram`. Cela applique la sécurité des types et empêche les mélanges accidentels.
Marquage Générique pour la Réutilisabilité
Pour éviter de répéter le schéma de marquage pour chaque type, vous pouvez créer un type d'aide générique :
type Brand = K & { readonly __brand: unique symbol; };
// Définir Kilogram en utilisant le type Brand générique
type Kilogram = Brand;
// Définir Gram en utilisant le type Brand générique
type Gram = Brand;
// Fonction d'aide pour créer des valeurs Kilogram
const Kilogram = (value: number) => value as Kilogram;
// Fonction d'aide pour créer des valeurs Gram
const Gram = (value: number) => value as Gram;
const kg: Kilogram = Kilogram(10);
const g: Gram = Gram(10000);
// Ceci provoquera toujours une erreur TypeScript
// const kg2: Kilogram = g; // Type 'Gram' is not assignable to type 'Kilogram'.
console.log(kg, g);
Cette approche simplifie la syntaxe et facilite la définition cohérente des types marqués.
Cas d'Utilisation Avancés et Considérations
Marquage d'Objets
Le marquage nominal peut également être appliqué aux types d'objets, pas seulement aux types primitifs comme les nombres ou les chaînes.
interface User {
id: number;
name: string;
}
const UserIDBrand: unique symbol = Symbol();
type UserID = number & { readonly [UserIDBrand]: true };
interface Product {
id: number;
name: string;
}
const ProductIDBrand: unique symbol = Symbol();
type ProductID = number & { readonly [ProductIDBrand]: true };
// Fonction attendant un UserID
function getUser(id: UserID): User {
// ... implémentation pour récupérer l'utilisateur par ID
return {id: id, name: "Example User"};
}
const userID = 123 as UserID;
const productID = 456 as ProductID;
const user = getUser(userID);
// Ceci provoquerait une erreur si décommenté
// const user2 = getUser(productID); // Argument of type 'ProductID' is not assignable to parameter of type 'UserID'.
console.log(user);
Cela empêche de passer accidentellement un `ProductID` là où un `UserID` est attendu, même si les deux sont finalement représentés comme des nombres.
Travail avec des Bibliothèques et des Types Externes
Lorsque vous travaillez avec des bibliothèques externes ou des API qui ne fournissent pas de types marqués, vous pouvez utiliser des assertions de type pour créer des types marqués à partir de valeurs existantes. Cependant, soyez prudent lorsque vous faites cela, car vous affirmez essentiellement que la valeur est conforme au type marqué, et vous devez vous assurer que c'est bien le cas.
// Supposons que vous recevez un nombre d'une API qui représente un UserID
const rawUserID = 789; // Nombre provenant d'une source externe
// Créer un UserID marqué à partir du nombre brut
const userIDFromAPI = rawUserID as UserID;
Considérations d'Exécution
Il est important de se rappeler que le marquage nominal dans TypeScript est purement une construction au moment de la compilation. Les marques (symboles uniques) sont effacées pendant la compilation, il n'y a donc pas de surcharge d'exécution. Cependant, cela signifie également que vous ne pouvez pas vous fier aux marques pour la vérification des types au moment de l'exécution. Si vous avez besoin d'une vérification des types au moment de l'exécution, vous devrez implémenter des mécanismes supplémentaires, tels que des garde-types personnalisés.
Garde-Types pour la Validation d'Exécution
Pour effectuer une validation d'exécution des types marqués, vous pouvez créer des garde-types personnalisés :
function isKilogram(value: number): value is Kilogram {
// Dans un scénario réel, vous pourriez ajouter des vérifications supplémentaires ici,
// comme s'assurer que la valeur est dans une plage valide pour les kilogrammes.
return typeof value === 'number';
}
const someValue: any = 15;
if (isKilogram(someValue)) {
const kg: Kilogram = someValue;
console.log("Value is a Kilogram:", kg);
} else {
console.log("Value is not a Kilogram");
}
Cela vous permet de réduire en toute sécurité le type d'une valeur au moment de l'exécution, en vous assurant qu'elle est conforme au type marqué avant de l'utiliser.
Avantages du Marquage Nominal
- Sécurité de type améliorée : Empêche les substitutions de types involontaires et réduit le risque d'erreurs logiques.
- Clarté de code améliorée : Rend le code plus lisible et plus facile à comprendre en distinguant explicitement les types ayant la même représentation sous-jacente.
- Temps de débogage réduit : Détecte les erreurs liées aux types au moment de la compilation, économisant ainsi du temps et des efforts lors du débogage.
- Confiance accrue dans le code : Offre une plus grande confiance dans la correction de votre code en appliquant des contraintes de type plus strictes.
Limitations du Marquage Nominal
- Uniquement au moment de la compilation : Les marques sont effacées pendant la compilation, elles ne fournissent donc pas de vérification des types au moment de l'exécution.
- Nécessite des assertions de type : La création de types marqués nécessite souvent des assertions de type, qui peuvent potentiellement contourner la vérification des types si elles sont utilisées incorrectement.
- Excès de code répétitif : La définition et l'utilisation de types marqués peuvent ajouter un peu de code répétitif à votre code, bien que cela puisse être atténué avec des types d'aide génériques.
Bonnes Pratiques pour l'Utilisation des Marques Nominales
- Utiliser le marquage générique : Créez des types d'aide génériques pour réduire le code répétitif et assurer la cohérence.
- Utiliser des garde-types : Implémentez des garde-types personnalisés pour la validation d'exécution si nécessaire.
- Appliquer les marques judicieusement : N'abusez pas du marquage nominal. Ne l'appliquez que lorsque vous avez besoin d'un contrôle de type plus strict pour prévenir les erreurs logiques.
- Documenter clairement les marques : Documentez clairement le but et l'utilisation de chaque type marqué.
- Considérer les performances : Bien que le coût d'exécution soit minime, le temps de compilation peut augmenter avec une utilisation excessive. Profilez et optimisez si nécessaire.
Exemples dans Différents Secteurs et Applications
Le marquage nominal trouve des applications dans divers domaines :
- Systèmes financiers : Différencier les différentes devises (USD, EUR, GBP) et types de comptes (Épargne, Courant) pour prévenir les transactions et calculs incorrects. Par exemple, une application bancaire pourrait utiliser des types nominaux pour s'assurer que les calculs d'intérêts ne sont effectués que sur les comptes d'épargne et que les conversions de devises sont appliquées correctement lors du transfert de fonds entre comptes dans différentes devises.
- Plateformes de commerce électronique : Différencier les identifiants de produits, les identifiants de clients et les identifiants de commandes pour éviter la corruption des données et les vulnérabilités de sécurité. Imaginez affecter accidentellement les informations de carte de crédit d'un client à un produit – les types nominaux peuvent aider à prévenir de telles erreurs désastreuses.
- Applications de santé : Séparer les identifiants de patients, les identifiants de médecins et les identifiants de rendez-vous pour assurer la bonne association des données et prévenir les mélanges accidentels de dossiers patients. Ceci est crucial pour maintenir la confidentialité des patients et l'intégrité des données.
- Gestion de la chaîne d'approvisionnement : Différencier les identifiants d'entrepôts, les identifiants d'expédition et les identifiants de produits pour suivre les marchandises avec précision et prévenir les erreurs logistiques. Par exemple, s'assurer qu'une expédition est livrée au bon entrepôt et que les produits de l'expédition correspondent à la commande.
- Systèmes IoT (Internet des Objets) : Différencier les identifiants de capteurs, les identifiants d'appareils et les identifiants d'utilisateurs pour assurer la collecte et le contrôle corrects des données. Ceci est particulièrement important dans les scénarios où la sécurité et la fiabilité sont primordiales, comme dans l'automatisation de la maison intelligente ou les systèmes de contrôle industriels.
- Jeux : Discriminer les identifiants d'armes, les identifiants de personnages et les identifiants d'objets pour améliorer la logique du jeu et prévenir les exploits. Une simple erreur pourrait permettre à un joueur d'équiper un objet destiné uniquement aux PNJ, perturbant ainsi l'équilibre du jeu.
Alternatives au Marquage Nominal
Bien que le marquage nominal soit une technique puissante, d'autres approches peuvent obtenir des résultats similaires dans certaines situations :
- Classes : L'utilisation de classes avec des propriétés privées peut fournir un certain degré de typage nominal, car les instances de différentes classes sont intrinsèquement distinctes. Cependant, cette approche peut être plus verbeuse que le marquage nominal et peut ne pas convenir à tous les cas.
- Enum : L'utilisation des énumérations TypeScript fournit un certain degré de typage nominal au moment de l'exécution pour un ensemble spécifique et limité de valeurs possibles.
- Types littéraux : L'utilisation de types littéraux de chaînes ou de nombres peut contraindre les valeurs possibles d'une variable, mais cette approche ne fournit pas le même niveau de sécurité de type que le marquage nominal.
- Bibliothèques externes : Des bibliothèques comme `io-ts` offrent des capacités de vérification et de validation des types au moment de l'exécution, qui peuvent être utilisées pour appliquer des contraintes de type plus strictes. Cependant, ces bibliothèques ajoutent une dépendance d'exécution et peuvent ne pas être nécessaires dans tous les cas.
Conclusion
Le marquage nominal TypeScript offre un moyen puissant d'améliorer la sécurité des types et de prévenir les erreurs logiques en créant des définitions de types opaques. Bien que ce ne soit pas un remplacement pour le véritable typage nominal, il offre une solution de contournement pratique qui peut améliorer considérablement la robustesse et la maintenabilité de votre code TypeScript. En comprenant les principes du marquage nominal et en l'appliquant judicieusement, vous pouvez écrire des applications plus fiables et sans erreurs.
N'oubliez pas de prendre en compte les compromis entre la sécurité des types, la complexité du code et la surcharge d'exécution lorsque vous décidez d'utiliser ou non le marquage nominal dans vos projets.
En intégrant les meilleures pratiques et en considérant attentivement les alternatives, vous pouvez exploiter le marquage nominal pour écrire du code TypeScript plus propre, plus maintenable et plus robuste. Adoptez la puissance de la sécurité des types et créez de meilleurs logiciels !