AccueilClientsExpertisesBlogOpen SourceJobsContact

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 ! 🛡

18 avenue Parmentier
75011 Paris
+33 1 43 57 39 11
hello@premieroctet.com

Suivez nos aventures

GitHub
Twitter
Flux RSS

Naviguez à vue