TypeScript le système de types - 11 - Union vs intersection : quand utiliser | et quand utiliser &

Comprendre la différence entre union (|) et intersection (&) en TypeScript. Pourquoi l'intersection ajoute des propriétés et l'union les restreint.

11 - Union vs intersection : quand utiliser | et quand utiliser &

Ce que tu vas apprendre

  • Ce que | (union) et & (intersection) font réellement sur les types
  • Pourquoi l'intersection d'objets ajoute des propriétés (et pas l'inverse)
  • Les pièges avec les types primitifs et les unions
  • Comment choisir entre les deux dans des cas concrets

Prerequisites

Avoir lu les articles sur les discriminated unions et les utility types.


La confusion la plus courante

Quand un junior voit A | B, il pense "A et B combines". Quand il voit A & B, il pense "ce qui est commun entre A et B". C'est l'inverse.

| (union) signifie : "la valeur est de type A ou de type B". Tu ne peux utiliser que ce qui est commun aux deux.

& (intersection) signifie : "la valeur est de type A et de type B". Tu as acces a tout.

typescriptinterface HasName { name: string }
interface HasEmail { email: string }

// Union : l'un OU l'autre
type Contact = HasName | HasEmail
const c: Contact = { name: "Nicolas" } // ✅
// c.name → ❌ pas garanti (c pourrait etre HasEmail)
// c.email → ❌ pas garanti (c pourrait etre HasName)

// Intersection : l'un ET l'autre
type FullContact = HasName & HasEmail
const f: FullContact = { name: "Nicolas", email: "n@n.dev" } // ✅
// f.name → ✅ garanti
// f.email → ✅ garanti

Union en détail

L'union A | B accepte une valeur qui satisfait A, ou B, ou les deux. Mais tu ne peux acceder qu'aux propriétés communes sans narrowing.

typescriptinterface Dog {
  name: string
  breed: string
}

interface Cat {
  name: string
  indoor: boolean
}

type Pet = Dog | Cat

function greet(pet: Pet) {
  console.log(pet.name)   // ✅ commun aux deux
  console.log(pet.breed)  // ❌ n'existe pas sur Cat
  console.log(pet.indoor) // ❌ n'existe pas sur Dog
}

Pour acceder aux propriétés spécifiques, il faut narrower :

typescriptfunction describe(pet: Pet) {
  if ("breed" in pet) {
    // pet est Dog
    console.log(`${pet.name} est un ${pet.breed}`)
  } else {
    // pet est Cat
    console.log(`${pet.name} vit ${pet.indoor ? "dedans" : "dehors"}`)
  }
}

Unions de primitifs

Les unions de primitifs sont simples et courantes :

typescripttype StringOrNumber = string | number

function format(value: StringOrNumber): string {
  if (typeof value === "string") {
    return value.toUpperCase()
  }
  return value.toFixed(2)
}

Les unions litterales sont les plus utiles :

typescripttype Direction = "north" | "south" | "east" | "west"
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE"
type Status = "idle" | "loading" | "success" | "error"

Le compilateur refuse toute valeur qui n'est pas dans l'union.

Intersection en détail

L'intersection A & B créé un type qui combine toutes les propriétés de A et B. La valeur doit satisfaire les deux types simultanément.

typescriptinterface Timestamped {
  createdAt: Date
  updatedAt: Date
}

interface SoftDeletable {
  deletedAt: Date | null
}

type User = {
  id: string
  name: string
  email: string
}

type FullUser = User & Timestamped & SoftDeletable
// {
//   id: string
//   name: string
//   email: string
//   createdAt: Date
//   updatedAt: Date
//   deletedAt: Date | null
// }

C'est comme de la composition. Tu construis un type complexe en combinant des morceaux independants. C'est un pattern que j'utilise sur paltemps.fr pour ajouter des metadonnees aux entités sans dupliquer les champs dans chaque interface.

Intersection de types incompatibles

Quand deux types ont une propriété avec le meme nom mais des types différents, l'intersection peut devenir never :

typescripttype A = { id: string }
type B = { id: number }
type C = A & B
// C.id est string & number = never
// C est utilisable en theorie mais aucune valeur ne peut satisfaire id

Le compilateur ne refuse pas le type C, mais tu ne pourras jamais créer une valeur valide.

Intersection de primitifs

L'intersection de deux primitifs incompatibles donne never :

typescripttype Impossible = string & number // never

Aucune valeur n'est a la fois string et number. never represente cet ensemble vide (vu dans l'article 01).

Pourquoi c'est contre-intuitif

L'intuition mathematique nous trompe. En theorie des ensembles :

  • L'union de deux ensembles = tous les éléments des deux → plus grand
  • L'intersection de deux ensembles = les éléments communs → plus petit

Mais en TypeScript, on parle de l'ensemble des valeurs valides :

  • string | number : toutes les strings ET tous les numbers → plus de valeurs possibles → moins de propriétés accessibles
  • HasName & HasEmail : seulement les objets qui ont name ET email → moins de valeurs possibles → plus de propriétés accessibles

Plus le type est restrictif sur les valeurs (intersection), plus tu as d'informations sur la structure. Plus le type est permissif sur les valeurs (union), moins tu sais ce qui est dedans.

Patterns pratiques

Composition de types avec &

typescript// Types de base reutilisables
interface WithId { id: string }
interface WithTimestamps {
  createdAt: Date
  updatedAt: Date
}

// Composition
type User = WithId & WithTimestamps & {
  name: string
  email: string
}

type Post = WithId & WithTimestamps & {
  title: string
  content: string
  authorId: string
}

Extension de types tiers avec &

Quand tu veux ajouter des propriétés a un type que tu ne contrôles pas :

typescriptimport type { Request } from "express"

type AuthenticatedRequest = Request & {
  user: { id: string; role: string }
}

function handler(req: AuthenticatedRequest) {
  console.log(req.user.id) // ✅
  console.log(req.body)    // ✅ proprietes Express toujours la
}

Union pour les overloads simplifies

typescripttype Input = string | string[] | Record<string, string>

function normalize(input: Input): string[] {
  if (typeof input === "string") return [input]
  if (Array.isArray(input)) return input
  return Object.values(input)
}

Union vs intersection : guide de choix

Situation Utilise
Plusieurs états possibles | (union)
Valeurs litterales restreintes | (union)
Combiner des traits/mixins & (intersection)
Ajouter des propriétés a un type existant & (intersection)
Paramètres qui acceptent plusieurs formes | (union)
Retour qui garantit plusieurs propriétés & (intersection)

En général : | pour les paramètres d'entree (flexibilité), & pour les types de donnees internes (precision).


Résumé

  • | (union) : la valeur est l'un ou l'autre type — tu n'accedes qu'aux propriétés communes
  • & (intersection) : la valeur satisfait les deux types — tu accedes a toutes les propriétés
  • L'intersection de propriétés incompatibles produit never
  • L'intuition "union = plus, intersection = moins" s'inverse quand on parle des propriétés accessibles
  • Utilise | pour les paramètres flexibles et & pour la composition de types

Article précédent : 10 - Utility types

Article suivant : 12 - Enums vs unions litterales

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.