AccueilClients

Applications et sites

  • Application métiersIntranet, back-office...
  • Applications mobilesAndroid & iOS
  • Sites InternetSites marketings et vitrines
  • Expertises techniques

  • React
  • Expo / React Native
  • Next.js
  • Node.js
  • Directus
  • TypeScript
  • Open SourceBlogContactEstimer

    25 juillet 2023

    Les branded types avec TypeScript

    3 minutes de lecture

    Les branded types avec TypeScript

    Nous vous proposons dans cet article de découvrir un pattern avancé de TypeScript : les branded types. Nous allons voir comment les utiliser, et comment ils peuvent nous aider à améliorer la qualité de notre code.

    Typage nominatif vs structurel

    Afin de comprendre les branded types, il est important de comprendre la différence entre le typage nominatif et le typage structurel.

    En TypeScript, le typage est dit structurel, c'est-à-dire que le compilateur va vérifier non pas le nom du type, mais sa structure. Ainsi TypeScript ne fait pas la différence entre ces deux types :

    type Person = {
      name: string
      age: number
    }
    
    type Employee = {
      name: string
      age: number
    }
    

    Avec un language typé nominativement (comme PHP), ces deux types seraient différents car ils n'ont pas le même nom.

    Le problème

    Le fait que TypeScript soit un langage à typage structurel peut parfois poser problème. Prenons comme exemple ces deux types :

    type AccountNumber = number
    type PaymentAmount = number
    

    Ils sont structurellement identiques, mais ne représentent pas (au sens métier) la même chose :

    type AccountNumber = number
    type PaymentAmount = number
    
    function spend(accountNumber: AccountNumber, amount: PaymentAmount) {
      // ...
    }
    
    const amount: PaymentAmount = 100
    const accountNumber: AccountNumber = 321321
    
    // 💥 Pas d'erreur TS alors que l'on a inversé des données
    spend(amount, accountNumber)
    

    Ici on a interverti les deux variables lors de l'appel de spend(), problème : TypeScript ne lève pas d'erreur car les deux types sont bien structurellement identiques.

    Il serait donc intéressant de pouvoir les différencier : c'est là qu'interviennent les branded types.

    Les branded types

    Grâce à une intersection, nous pouvons "tagguer" nos types afin de les différencier structurellement :

    type AccountNumber = number & { __: 'AccountNumber' }
    type PaymentAmount = number & { __: 'PaymentAmount' }
    

    Nous pouvons ensuite créer des fonctions permettant de caster nos types grâce à un prédicat de type :

    function isAccountNumber(accountNumber: number): accountNumber is AccountNumber {
      return accountNumber.toString().length === 13
    }
    

    Ainsi le/la développeur(euse) sera obligé de passer par cette fonction afin de créer un AccountNumber :

    type AccountNumber = number & { _: 'AccountNumber' }
    const accountNumber = 1263548749287
    
    function logAccountNumber(accountNumber: AccountNumber) {
      console.log(accountNumber)
    }
    
    // TypeScript lève une erreur
    logAccountNumber(accountNumber)
    
    // TypeScript ne lève plus d'erreur car accountNumber est désormais de type AccountNumber
    logAccountNumber(isAccountNumber(accountNumber))
    

    Exemple concret

    Prenons un autre exemple plus parlant. Nous voulons obliger une personne à passer par une fonction permettant de valider un email avant de l'utiliser. Nous créons donc un type ValidEmail :

    type ValidEmail = string & { __: 'ValidEmail' }
    

    Il est alors possible d'utiliser ce type pour s'assurer que la fonction isValidEmail() a bien été appelée avant :

    const isValidEmail = (email: string): email is ValidEmail => {
      return email.includes('@')
    }
    
    const createUser = async (user: { email: ValidEmail }) => {
      return user
    }
    
    export const onSubmit = async (values: { email: string }) => {
      if (!isValidEmail(values.email)) {
        throw new Error('Email is invalid')
      }
    
      await createUser({
        email: values.email,
      })
    }
    

    Si l'on retire la vérification isValidEmail, TypeScript lèvera bien une erreur car la variable email n'aura pas été castée en ValidEmail :

    export const onSubmit = async (values: { email: string }) => {
      // 💥 Erreur TypeScript
      await createUser({
        email: values.email,
      })
    }
    

    Conclusion

    Les branded types (aussi connus sous le nom d'opaque, tagged, nominal types) permettent donc de simuler le typage nominatif en ajoutant "un tag" à la structure du type. Ce pattern permet de rajouter une couche de validation sur des parties de code sensible.

    Si ce type de patterns vous intéresse, n'hésitez pas à participer à notre formation TypeScript !

    À vos types ! 🛡

    À découvrir également

    Premier Octet vous accompagne dans le développement de vos projets avec typescript

    Discuter de votre projet typescript