Code : | Sélectionner tout |
1 2 3 4 5 6 7 | type A = { x: number } type B = { x: number } |
Dans ce billet, nous nous appuyons sur ce travail pour produire un moyen de marquer un type, en fournissant un moyen automatisé et facile à utiliser pour rendre un type nominal. Le marquage se concentre uniquement sur le système de type, plutôt que d'introduire des champs d'exécution comme dans l'article précédent, ce qui est un avantage majeur par rapport à l'approche précédente.
Quel est le problème ?
Le marquage, également connu sous le nom de types opaques, permet de différencier les types dans TypeScript qui, autrement, seraient classés comme le même type. Par exemple
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | type A = { x: number, y: boolean, z: string, } type B = { x: number, y: boolean, z: string, } |
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | const fn = (a: A) => { console.log('do something with A') } const obj: B = { x: 1, y: true, z: 'hello' } fn(obj) // absolutely fine, even though fn accepts types of A and obj is of type B! |
Mais que se passe-t-il si nous devons distinguer A et B ? Et si, d'un point de vue conceptuel, ils doivent être différents ? Et si nous faisons quelque chose de fantaisiste avec A et B dont TypeScript n'est pas au courant, mais que nous avons besoin que les types soient différents ? C'est exactement la situation dans laquelle nous nous sommes trouvés dernièrement !
Nous avons besoin d'une marque pour faire exactement cela.
La solution
Comme dans l'article sur les types mappés TypeScript, la clé réside dans la création d'un champ avec le nom d'un symbole qui servira d'identifiant. Cependant, avec le marquage, nous n'avons besoin de ce champ qu'au niveau du type plutôt qu'au niveau de l'exécution. Comme les types sont effacés après la compilation, nous devons ajouter ce champ à un type sans modifier les données d'exécution. Casting, quelqu'un ?
Tout d'abord, présentons le champ brand.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | const brand = Symbol('brand') // keep this private!! type A = { x: number, y: boolean, z: string, } & { [brand]: 'A' } |
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | const fn = (a: A) => { console.log('do something with A') } const obj: B = { x: 1, y: true, z: 'hello' } fn(obj) // Error! |
Code : | Sélectionner tout |
Argument of type 'B' is not assignable to parameter of type 'A'. Property '[brand]' is missing in type 'B' but required in type '{ [brand]: "a"; }'.ts(2345)
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 | type B = { x: number, y: boolean, z: string, } & { [brand]: 'B' } |
Essayons maintenant la fonction à nouveau :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | const fn = (a: A) => { console.log('do something with A') } const obj: B = { x: 1, y: true, z: 'hello' } fn(obj) // Error! |
Code : | Sélectionner tout |
Argument of type 'B' is not assignable to parameter of type 'A'. Type 'B' is not assignable to type '{ [brand]: "A"; }'. Types of property '[brand]' are incompatible. Type '"B"' is not assignable to type '"A"'.ts(2345)
Voyons ce qui se passe si la brand est la même :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | type A = { x: number, y: boolean, z: string, } & { [brand]: 'foobar' } type B = { x: number, y: boolean, z: string, } & { [brand]: 'foobar' } const fn = (a: A) => { console.log('do something with A') } const obj: B = { x: 1, y: true, z: 'hello' } fn(obj) // absolutely fine! |
Rendons-le générique !
Génial, ça marche. Mais c'est un exemple de jouet, qui n'est pas adapté à la production. Créons un type Brand qui peut marquer n'importe quel type :
Code : | Sélectionner tout |
1 2 3 4 5 | const brand = Symbol('brand') // keep this private! type Brand<T, U> = T & { [brand]: U } |
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 | type A_Unbranded = { x: number, y: boolean, z: string, } type A = Brand<A_Unbranded, 'A'> // { // x: number; // y: boolean; // z: string; // } & { // [brand]: "A"; // } |
Code : | Sélectionner tout |
type RemoveBrand<T> = T[Exclude]
Utilisation dans le monde réel
Mettons cela en pratique. Nous avons une classe qui a besoin d'une marque pour identifier son type lorsqu'elle traite avec des types mappés.
Pour plus de simplicité, réduisons la classe à une classe Dog :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 | class Dog { constructor(public name: string) {} } type DogBranded = Brand<Dog, 'Dog'> const dog = new DogBranded('Spot') // Error! |
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | type Ctor<T> = new (...args: any[]) => T const addBrand = <T>(ctor: Ctor<T>, name: string) => { return ctor as Ctor<Brand<T, typeof name>> } const DogBranded = addBrand(Dog, 'Dog') const dog = new DogBranded('Spot') // ok |
Nous pouvons exporter (export) le type DogBranded pour permettre au monde extérieur d'utiliser notre classe tout en s'assurant qu'elle est toujours marquée :
Code : | Sélectionner tout |
export type DogExported = typeof DogBranded
Code : | Sélectionner tout |
1 2 3 4 | const removeBrand = <T>(value: T) => { return value as RemoveBrand<T> } |
Et voilà : un moyen sûr de marquer et de démasquer vos types en TypeScript 😃
Nous avons publié ce travail sous la forme d'une bibliothèque à laquelle vous pouvez accéder via NPM !
Chez Prosopo, nous utilisons le marquage TypeScript pour fortifier nos types et faire un mappage de type intelligent pour notre validateur de type d'exécution qui sera bientôt publié.
À propos de Prosopo
Prosopo est un projet open source dédié à la création d'un réseau de détection de bots alimenté par la communauté. Les services de détection de bots existants sont soit invasifs pour la vie privée, soit coûteux. Notre objectif est de changer cela en créant un réseau de sites web capables de détecter les bots sans compromettre la vie privée des utilisateurs. Plus il y a de sites web qui rejoignent le réseau, plus la détection des bots devient puissante.
Notre approche de la détection des bots s'inspire des systèmes CAPTCHA traditionnels, mais avec une nuance. Nous collectons le moins d'informations possible sur l'utilisateur, la plupart des détections étant effectuées dans le navigateur. Des preuves cryptographiques sont utilisées pour envoyer le minimum d'informations au serveur, et le serveur peut vérifier la preuve sans avoir besoin de connaître quoi que ce soit sur l'utilisateur.
Source : "TypeScript: Branded Types" (Proposo)
Et vous ?
Quel est votre avis sur le sujet ?
Voir aussi :
Apprendre les différentes fonctionnalités de TypeScript
Microsoft annonce la disponibilité de TypeScript 5.5 Beta. Cette version apporte les prédicats de type inférés et réduit le flux de controle pour les accès indexés constants
Cinq vérités inconfortables à propos de TypeScript selon Stefan Baumgartner, auteur de livres sur le langage de programmation