Vous savez déjà que pour obtenir plus de fiabilité et de sécurité dans votre base de code, vous pouvez utiliser Typescript, mais même avec cela, il y a de la place pour l'amélioration !
Un concept qui peut augmenter cette fiabilité est les Types Marqués.
Ils fournissent un moyen de créer une spécificité et une unicité plus profondes pour modéliser vos données au-delà des types primitifs de base.
Dans cet article, nous explorerons ce que sont les types marqués, comment les utiliser, quelques cas d'utilisation et un défi pour assurer les apprentissages.
Le Problème
Typescript aide à garantir que les données correctes sont transmises à travers votre flux d'application, mais souvent, les données ne sont pas assez spécifiques. Imaginons le scénario suivant : une application où les utilisateurs sont propriétaires de plusieurs autres structures de données.
type User = { id: string name: string}
type Post = { id: string ownerId: string; comments: Comments[]}
type Comments = { id: string timestamp: string body: string authorId: string}
Cela définit la relation entre un type User et le Post où un Post peut avoir de nombreux commentaires écrits par un utilisateur.
Le problème ici est que la propriété qui identifie chaque objet : User["id"], Post["id"] et Comments["id"] sont en fait juste des chaînes de caractères, donc il n'y a aucun moyen pour Typescript de détecter si vous passez les mauvaises données, pour Typescript, elles sont toutes les mêmes et interchangeables.
async function getCommentsForPost(postId: string, authorId: string) {
const response = await api.get(`/author/${authorId}/posts/${postId}/comments`)
return response.data
}
const comments = await getCommentsForPost(user.id, post.id) // Ceci est correct pour Typescript
Le snippet ci-dessus montre une fonction qui reçoit deux arguments postId et authorId, tous deux ne sont que des chaînes de caractères. Mais il y a une erreur ici, l'avez-vous remarquée ? Lors de l'appel de la fonction getCommentsForPost, j'ai passé les arguments dans le mauvais ordre, mais pour Typescript, c'est tout pareil, donc pas d'erreur.
Cet appel de fonction aura la mauvaise réponse à l'exécution, mais comment pouvez-vous permettre à Typescript de détecter ces erreurs ? Nous avons besoin d'un moyen de spécifier les différents types d'ID.
Types Marqués ?
Un modèle évolué de la communauté qui est couramment utilisé pour ce cas est les Types Marqués. L'idée est de créer un type de données plus spécifique et unique avec une plus grande clarté et spécificité, cela est accompli en ajoutant des attributs ou des étiquettes à un type existant pour créer un nouveau type plus spécifique.
Les marques peuvent être ajoutées à un type en utilisant une union du type de base et un littéral d'objet avec une propriété marquée. Par exemple :
type Brand<K, T> = K & { __brand: T }
type UserID = Brand<string, "UserId">
type PostID = Brand<string, "PostId">
Cela crée un nouveau type appelé UserID, qui est associé à la marque UserId. Une variable réelle préfixée avec ce type de marque doit correspondre au type spécifique pour être utilisée, réécrivons l'exemple précédent en utilisant le nouvel assistant Brand :
type UserID = Brand<string, "UserId">
type PostID = Brand<string, "PostId">
type CommentID = Brand<string, "CommentId">
type User = { id: UserID; name: string }
type Post = { id: PostID; ownerId: string; comments: Comments[] }
type Comments = { id: CommentID; timestamp: string; body: string; authorId: UserID }
async function getCommentsForPost(postId: PostID, authorId: UserID) {
const response = await api.get(`/author/${authorId}/posts/${postId}/comments`)
return response.data
}
const comments = await getCommentsForPost(user.id, post.id) // ❌ Cela échoue car `user.id` est de type UserID et non PostID comme attendu
// ^Argument of type 'UserID' is not assignable to parameter of type 'PostID'.
// Type 'UserID' is not assignable to type '{ __brand: "PostId"; }'.
// Types of property '__brand' are incompatible.
// Type '"UserId"' is not assignable to type '"PostId"'.
Vérifiez cet exemple dans le playground Typescript LIEN
C'est une bonne solution au problème actuel, mais elle présente quelques inconvénients :
- La propriété __brand utilisée pour "taguer" le type est une propriété uniquement à la compilation.
- La propriété __brand est toujours affichée via Intellisense, ce qui peut poser des problèmes si un développeur essaie de l'utiliser, car elle ne sera pas présente à l'exécution.
- Il est possible de dupliquer les types marqués car il n'y a pas de sécurité sur la propriété __brand.
declare const __brand: unique symbol
type Brand<B> = { [__brand]: B }
export type Branded<T, B> = T & Brand<B>
Vérifiez ce playground LIEN pour voir comment cela fonctionne
Pourquoi les Types Marqués sont-ils utiles ?
Les types marqués peuvent apporter plusieurs avantages par rapport à l'utilisation de types primitifs, tels que :
- Clarté : Ils offrent une plus grande expressivité et clarté quant à l'utilisation prévue des variables. Par exemple, un type marqué "NomUtilisateur" peut garantir que la variable contient uniquement des noms d'utilisateur valides, évitant ainsi les problèmes liés aux caractères ou longueurs invalides.
- Sécurité et exactitude : Ils peuvent aider à prévenir les problèmes en facilitant la compréhension du code et en détectant les erreurs liées à l'incompatibilité ou à la non-concordance des types.
- Maintenabilité : Ils peuvent rendre les bases de code plus maintenables en réduisant l'ambiguïté et la confusion, ce qui facilite la compréhension du code par les autres. En utilisant des types marqués, les développeurs peuvent communiquer leur intention plus clairement et éviter les malentendus ou les mauvaises utilisations des variables. De plus, les types marqués peuvent faciliter le refactoring en fournissant une distinction claire entre les différents types de données et leurs utilisations respectives.
Cas d'utilisation
Les types marqués peuvent être utilisés dans de nombreux scénarios. Voici quelques exemples :
Validation personnalisée
Ils peuvent aider à la création de fonctions de validation pour s'assurer que les données de l'utilisateur respectent un format standard ou souhaité. Par exemple, les marques peuvent être utilisées pour valider les adresses e-mail comme étant dans un format correct :
type EmailAdress = Brand<string, "EmailAdress">
function emailValide(email: string): EmailAdress {
// logique de validation de l'adresse e-mail ici
return email as EmailAdress;
}
Si à un moment donné l'e-mail n'est pas de la marque appropriée ou si l'entrée est ambiguë, l'utilisateur recevra un message d'échec.
Modélisation de domaine
Les types marqués excellent dans la modélisation de domaines qui peuvent être traduits en une expérience de codage plus expressive dans l'ensemble. Par exemple, une ligne de fabrication de voitures pourrait utiliser des types marqués pour différentes caractéristiques ou types de voitures :
type CarBrand = Brand<string, "CarBrand">
type MotorType = Brand<string, "MotorType">
type CarModel = Brand<string, "CarModel">
type CarColor = Brand<string, "CarColor">
Avec cette approche, le vérificateur de types peut désormais imposer une meilleure sécurité des types :
function createCar(carBrand: CarBrand, carModel: CarModel, motorType: MotorType, color: CarColor): Car {
// ...
}
const car = createCar("Toyota", "Corolla", "Diesel", "Rouge") // Erreur :
// "Diesel" n'est pas de type "MotorType"
Réponses et requêtes d'API
Les points de terminaison d'API peuvent utiliser des marques pour personnaliser les réponses et les requêtes des appels d'API. Les types marqués ou étiquetés fonctionnent bien dans ce contexte car l'étiquetage provient des fournisseurs d'API. Dans un exemple de code hypothétique, ici, nous utiliserons une marque avec une API spécifique pour différencier les appels d'API réussis et échoués :
type ApiSuccess<T> = T & { __apiSuccessMark: true }
type ApiFailed = { code: number; message: string; erreur: Error; } & { __apiFailedMark: true };
type ApiResponse<T> = ApiSuccess<T> | ApiFailed;
Vous pouvez maintenant utiliser ce type de réponse, évitant les malentendus :
const reponse: ApiResponse<string> = await fetchSomeEndpoint();
if (isApiSuccess(reponse)) {
// gérer la réponse réussie
}
if (isApiFailed(reponse)) {
// enregistrer le message d'erreur
}
Conclusion
Les types marqués sont des fonctionnalités puissantes de TypeScript qui peuvent aider à améliorer la sécurité des types, la maintenabilité et la clarté du code. Ils peuvent offrir un meilleur contrôle sur la forme des données, proposer des types plus expressifs et permettre des vérifications de sécurité à la compilation qui réduisent le temps de débogage et préviennent les erreurs à l'exécution. En utilisant judicieusement les types marqués, vous pouvez créer des projets TypeScript plus efficaces, évolutifs et sûrs.