Vol. 5 · № 03 · Un atelier TypeScriptA TypeScript workshop

La variance, ou dans quel sens va la flèche ? Variance, or which way does the arrow go?

Cat est un sous-type d'Animal. Mais un Cat[] est-il un Animal[] ? Oui (covariance), pas l'inverse. Et un handler d'Animal passe pour un handler de Cat — la flèche s'inverse (contravariance), et c'est ce qui rend la substitution sûre. Sorties covariantes, entrées contravariantes. Avec deux trous assumés : tableaux mutables covariants, paramètres de méthode bivariants. Cat is a subtype of Animal. But is a Cat[] an Animal[]? Yes (covariance), not the reverse. And an Animal handler stands in for a Cat handler — the arrow flips (contravariance), and that's what makes substitution safe. Outputs covariant, inputs contravariant. With two owned holes: covariant mutable arrays, bivariant method parameters.

AudienceAudience
Dev à l'aise avec les génériques contraints, prêt à voir le sens du sous-typage Dev comfortable with constrained generics, ready to see subtyping's direction
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●●●● VarianceCovarianceTradeoff
SommaireContents ·
№ 01CadrageFraming3 min

Un Cat est un Animal. Mais le sous-typage a une direction dans un conteneur.A Cat is an Animal. But subtyping has a direction inside a container.

Le structurel du Vol 1 a établi que Cat est un sous-type d'Animal. Question du numéro : cette flèche se propage-t-elle aux conteneurs et aux fonctions ? L'intuition dit « oui, dans les deux sens » — et se trompe. Un Cat[] est bien un Animal[] (on peut traiter des chats comme des animaux), mais l'inverse est refusé. La relation n'est pas symétrique : elle a un sens. C'est ça, la variance — et le sens s'inverse là où on s'y attend le moins.Vol 1's structural typing established that Cat is a subtype of Animal. The issue's question: does that arrow propagate to containers and functions? Intuition says 'yes, both ways' — and is wrong. A Cat[] is indeed an Animal[] (you can treat cats as animals), but the reverse is refused. The relation isn't symmetric: it has a direction. That's variance — and the direction flips where you least expect it.

La relation n'est pas symétrique : un seul sens passeThe relation isn't symmetric: only one way passes
class Animal { name = "" }
class Cat extends Animal { meow() {} }

const cats: Cat[] = [new Cat()];
const animals: Animal[] = cats;     // ✓ ce sens marche…
const backToCats: Cat[] = animals;  // ✗ …mais PAS celui-ci :
//   Type 'Animal[]' is not assignable to type 'Cat[]'  (un Animal n'est pas un Cat)
Le sous-typage traverse-t-il le tableau ?Does subtyping cross the array?
Cat[] → Animal[]
✓ accepté✓ accepted
la flèche est préservée — covariancethe arrow is preserved — covariance
Animal[] → Cat[]
✗ refusé✗ refused
un Animal n'est pas forcément un Catan Animal isn't necessarily a Cat

Là où Cat <: Animal, le tableau préserve le sens : Cat[] <: Animal[]. C'est la covariance. Le réflexe du numéro : ne jamais supposer qu'une relation de sous-typage traverse un générique dans les deux sens.Where Cat <: Animal, the array preserves the direction: Cat[] <: Animal[]. That's covariance. The issue's reflex: never assume a subtyping relation crosses a generic both ways.

La covariance — le sens préservé, nomméCovariance — the preserved direction, named
// Cat <: Animal   (Cat est un sous-type d'Animal — Vol 1)
//   donc  Cat[]    <: Animal[]    ✓  la flèche est PRÉSERVÉE → covariance
//   mais  Animal[] </: Cat[]      ✗  pas l'inverse

const animals: Animal[] = cats;     // ✓ traiter des Cat comme des Animal : toujours sûr
Le réflexe du numéroThe issue's reflex

« Dans quel sens le sous-typage traverse-t-il ce générique ? » Sortie et lecture préservent la flèche (covariant) ; entrée et paramètre l'inversent (contravariant). Avant d'assigner un conteneur à un autre, demande-toi de quel côté la valeur est lue ou écrite."In which direction does subtyping cross this generic?" Output and read positions preserve the arrow (covariant); input and parameter positions flip it (contravariant). Before assigning one container to another, ask which side the value is read or written.

Pourquoi ce numéro clôt le Vol 5.Why this issue closes Vol 5.

Le 5.1 fait voyager le type, le 5.2 le borne à une capacité. Restait la question fine : quand un type paramétré se glisse à la place d'un autre, quelle direction le sous-typage suit-il ? La variance répond — et révèle les deux endroits où TypeScript troque, sciemment, un peu de sûreté contre de l'ergonomie.5.1 makes the type travel, 5.2 bounds it to a capability. The subtle question remained: when one parameterized type stands in for another, which direction does subtyping follow? Variance answers — and reveals the two spots where TypeScript knowingly trades a bit of soundness for ergonomics.

№ 02La surpriseThe surprise4 min

Les paramètres de fonction inversent la flèche. Et c'est exactement ce qui protège.Function parameters flip the arrow. And that's exactly what protects you.

Voici le cœur contre-intuitif du numéro. Les tableaux préservent le sens ; les paramètres de fonction l'inversent. Un handler qui accepte n'importe quel Animal peut remplacer un handler de Cat — parce qu'il accepte les Cat, et davantage. Donc (a: Animal) => void est un sous-type de (a: Cat) => void : la flèche s'est retournée. L'inverse est refusé, et pour une bonne raison : un handler de Cat appelé avec un Dog planterait.Here's the issue's counter-intuitive heart. Arrays preserve the direction; function parameters flip it. A handler that accepts any Animal can replace a Cat handler — because it accepts Cats, and more. So (a: Animal) => void is a subtype of (a: Cat) => void: the arrow turned around. The reverse is refused, for good reason: a Cat handler called with a Dog would crash.

Un handler d'Animal passe pour un handler de CatAn Animal handler stands in for a Cat handler
type CatHandler = (a: Cat) => void;

const handleAnimal = (a: Animal) => console.log(a.name);
const h: CatHandler = handleAnimal;   // ✓ un handler d'Animal accepte AUSSI les Cat
//   la flèche s'est inversée : (Animal) => void  <:  (Cat) => void

En position de paramètre, accepter plus (un Animal quelconque) rend la fonction plus substituable, pas moins. Le sous-typage s'inverse : (Animal) => void <: (Cat) => void. C'est la contravariance — l'exact opposé du tableau.In parameter position, accepting more (any Animal) makes the function more substitutable, not less. Subtyping flips: (Animal) => void <: (Cat) => void. That's contravariance — the exact opposite of the array.

Ce qu'on lit préserve la flèche ; ce qu'on fournit l'inverse. Un consommateur plus général est plus substituable — voilà toute la contravariance.What you read preserves the arrow; what you supply flips it. A more general consumer is more substitutable — that's the whole of contravariance.
№ 03La mécaniqueThe mechanics5 min

La position décide du sens : sortie covariante, entrée contravariante.Position decides the direction: output covariant, input contravariant.

Une seule règle gouverne tout, et elle découle de la substitution. Là où un type est produit — un retour, la valeur d'un Promise, un élément qu'on lit — la flèche est préservée : covariant. Là où un type est consommé — un paramètre — elle s'inverse : contravariant. Et quand T est à la fois lu et écrit (l'élément d'un tableau mutable), il est invariant en théorie : aucun des deux sens n'est sûr. Toute la variance se déduit de « qui fournit, qui reçoit ».One rule governs all, and it follows from substitution. Where a type is produced — a return, a Promise's value, an element you read — the arrow is preserved: covariant. Where a type is consumed — a parameter — it flips: contravariant. And when T is both read and written (a mutable array's element), it's invariant in principle: neither direction is safe. All of variance follows from 'who supplies, who receives'.

La position dans la signature fixe le sensPosition in the signature sets the direction
// SORTIE / lecture → covariant (la flèche est préservée)
type Producer<T> = () => T;
const pc: Producer<Animal> = (() => new Cat()) as Producer<Cat>;   // ✓ Cat <: Animal

// ENTRÉE / paramètre → contravariant (la flèche s'inverse)
type Consumer<T> = (x: T) => void;
const cc: Consumer<Cat> = ((x: Animal) => {}) as Consumer<Animal>; // ✓ Animal "<:" Cat ici
La règle, déduite de la substitutionThe rule, derived from substitution
// La substitution (Liskov), en une phrase :
//   on peut FOURNIR plus (un supertype) là où une entrée est consommée,
//   et RENDRE moins (un sous-type) là où une sortie est produite.
//
//   () => Cat        <:  () => Animal       (retour : covariant)
//   (Animal) => void <:  (Cat) => void      (paramètre : contravariant)
Où va la flèche, par positionWhere the arrow goes, by position
Le sensThe direction
Sortie / retourOutput / returncovariant (préservée)covariant (preserved)
Entrée / paramètreInput / parametercontravariant (inversée)contravariant (flipped)
Lu ET écritRead AND writteninvariant (aucun sens sûr)invariant (neither safe)
ExemplesExamples
() => Tcovariant en Tcovariant in T
(x: T) => voidcontravariant en Tcontravariant in T
T[]invariant en théorie (cf. 04)invariant in theory (see 04)
Les voisins, seulement nommés.The neighbors, only named.

Le principe sous-jacent est la substitution de Liskov. Un readonly T[] est proprement covariant (on ne fait que lire), là où le T[] mutable ne l'est qu'au prix d'un trou (numéro suivant). TypeScript permet désormais d'annoter la variance d'un paramètre de type avec in (contravariant) et out (covariant) — utile sur les types récursifs. Et l'extends ici (héritage de classe) n'est pas celui du 5.2 (contrainte).The underlying principle is Liskov substitution. A readonly T[] is properly covariant (you only read), whereas the mutable T[] is so only at the cost of a hole (next issue). TypeScript now lets you annotate a type parameter's variance with in (contravariant) and out (covariant) — handy on recursive types. And the extends here (class inheritance) isn't 5.2's (constraint).

№ 04Le piègeThe trap4 min

La bivariance des méthodes : le trou que TypeScript tolère sciemment.Method bivariance: the hole TypeScript knowingly tolerates.

strictFunctionTypes a rendu les paramètres de fonction contravariants — sains. Mais il y a une exception assumée : les paramètres écrits en syntaxe de méthode (m(x) au lieu de m: (x) => …) restent bivariants, c'est-à-dire acceptés dans les DEUX sens, y compris l'unsound. C'est un compromis historique, indispensable aux tableaux et aux handlers d'événements, mais c'est un trou réel. La même API change de sûreté selon qu'on l'écrit en méthode ou en propriété-fonction.strictFunctionTypes made function parameters contravariant — sound. But there's an owned exception: parameters written in method syntax (m(x) instead of m: (x) => …) stay bivariant, that is, accepted in BOTH directions, including the unsound one. It's a historical compromise, needed for arrays and event handlers, but it's a real hole. The same API changes safety depending on whether you write it as a method or a function-property.

Le paramètre en méthode accepte le sens dangereuxThe method parameter accepts the dangerous direction
interface Listener { handle(e: ClickEvent): void }      // ← écrit en MÉTHODE
interface Specific { handle(e: SpecificClick): void }   // exige un événement PLUS spécifique

declare const specific: Specific;
const l: Listener = specific;   // ✓ ACCEPTÉ — et c'est le trou :
l.handle(clickEvent);           //   handle reçoit un ClickEvent sans .detail() → 💥

Écrit en méthode, le paramètre de handle est vérifié bivariamment : le sens contravariant (sûr) et le sens covariant (dangereux) passent tous deux. Le second laisse passer un objet dont handle exige un événement plus spécifique, là où l'appelant, qui ne voit qu'un Listener, lui livrera un ClickEvent nu — sans .detail(). strictFunctionTypes ne couvre pas ce cas.Written as a method, handle's parameter is checked bivariantly: the contravariant (safe) direction and the covariant (dangerous) one both pass. The second lets through an object whose handle demands a more specific event, where the caller — who only sees a Listener — will hand it a bare ClickEvent, with no .detail(). strictFunctionTypes does not cover this case.

L'autre trou assumé : les tableaux mutables.The other owned hole: mutable arrays.

Le tableau est lu et écrit, donc invariant en théorie — mais TypeScript le rend covariant par confort. D'où : const a: Animal[] = cats (✓), puis a.push(new Dog()) (✓), et cats[1].meow() 💥 — un Dog s'est glissé dans un Cat[]. Un readonly Animal[] évite le piège (pas d'écriture). Comme la bivariance, c'est un compromis connu : la variance est l'endroit où TypeScript échange un peu de solidité contre beaucoup d'ergonomie — sache où.An array is read and written, hence invariant in theory — but TypeScript makes it covariant for convenience. Hence: const a: Animal[] = cats (✓), then a.push(new Dog()) (✓), and cats[1].meow() 💥 — a Dog slipped into a Cat[]. A readonly Animal[] avoids the trap (no writes). Like bivariance, it's a known compromise: variance is where TypeScript trades a bit of soundness for a lot of ergonomics — know where.

La variance est là où TypeScript admet ne pas être sound. Les méthodes bivariantes et les tableaux covariants sont des trous choisis — pas des bugs, mais des dettes qu'il faut connaître par leur nom.Variance is where TypeScript admits it isn't sound. Bivariant methods and covariant arrays are chosen holes — not bugs, but debts you must know by name.
№ 05Bilan & éditoWrap-up & editorial2 min

Le sens de la flèche — et les deux endroits où TypeScript ferme les yeux.The direction of the arrow — and the two spots where TypeScript looks away.

La variance, c'est la direction du sous-typage à travers un générique. Les sorties la préservent (covariant), les entrées l'inversent (contravariant) — et cette inversion, déroutante au début, est exactement ce qui rend la substitution sûre. TypeScript fait ensuite deux concessions assumées : tableaux mutables covariants, paramètres de méthode bivariants. Deux trous choisis — à connaître par leur nom.Variance is the direction of subtyping through a generic. Outputs preserve it (covariant), inputs flip it (contravariant) — and that flip, baffling at first, is exactly what makes substitution safe. TypeScript then makes two owned concessions: covariant mutable arrays, bivariant method parameters. Two chosen holes — to know by name.

Tu veux…You want to…Le sensThe directionCe que ça coûteWhat it costs
Passer un Cat[] pour un Animal[]Pass a Cat[] as an Animal[]Covariance — la flèche est préservée (sortie/lecture).Covariance — the arrow is preserved (output/read).Unsound si le conteneur est mutable (push).Unsound if the container is mutable (push).
Passer un handler d'Animal pour un de CatPass an Animal handler as a Cat oneContravariance — la flèche s'inverse (entrée/paramètre).Contravariance — the arrow flips (input/parameter).Rien : c'est le sens sûr, celui de Liskov.Nothing: it's the safe direction, Liskov's.
Des params de fonction vérifiés sainementSoundly checked function paramsstrictFunctionTypes (dans strict).strictFunctionTypes (in strict).Ne couvre pas les params écrits en méthode (bivariants).Doesn't cover params written as methods (bivariant).
Ne PAS supposerNOT assumeQue le sous-typage traverse un générique dans les deux sens.That subtyping crosses a generic both ways.Il a une direction — et les méthodes ont un trou toléré.It has a direction — and methods have a tolerated hole.
On produit un sous-type, on consomme un supertype. La flèche se préserve à la sortie et s'inverse à l'entrée — tout le reste de la variance n'en est que la conséquence.You produce a subtype, you consume a supertype. The arrow holds at the output and flips at the input — all the rest of variance is just its consequence.
Mot de l'éditeurFrom the editor

« Dans quel sens va la flèche ? »"Which way does the arrow go?"

Le structurel du Vol 1 nous a donné une flèche : Cat est un sous-type d'Animal. La variance est la question de ce que devient cette flèche quand on la fait passer à travers un conteneur ou une fonction. La réponse n'est pas « elle reste la même » : elle dépend de la position. Ce qu'on lit, ce qu'on retourne, préserve le sens — covariant, intuitif. Ce qu'on reçoit en paramètre l'inverse — contravariant, déroutant, jusqu'à ce qu'on voie qu'un consommateur plus général est plus substituable, pas moins. L'inversion n'est pas un caprice du compilateur : c'est la règle de Liskov, celle qui empêche un handler de Cat de recevoir un Dog.Vol 1's structural typing gave us an arrow: Cat is a subtype of Animal. Variance is the question of what becomes of that arrow when you push it through a container or a function. The answer isn't 'it stays the same': it depends on position. What you read, what you return, preserves the direction — covariant, intuitive. What you receive as a parameter flips it — contravariant, baffling, until you see that a more general consumer is more substitutable, not less. The flip isn't a compiler whim: it's Liskov's rule, the one that stops a Cat handler from receiving a Dog.

Et puis il y a les deux endroits où TypeScript ferme les yeux : les tableaux mutables, covariants par confort quand la rigueur les voudrait invariants ; les paramètres de méthode, bivariants par héritage quand strictFunctionTypes rend les autres contravariants. Ce ne sont pas des bugs — ce sont des dettes choisies, un peu de solidité échangée contre beaucoup d'ergonomie. Ainsi se ferme le Vol 5 : un type voyage (5.1), se contraint à une capacité (5.2), et traverse les génériques dans un sens qu'on sait désormais lire (5.3). Jusqu'ici, les types décrivaient et reliaient des valeurs. Le prochain volume franchit un seuil : calculer un type à partir d'un autre — mapped types, types conditionnels, le langage dans le langage. Le même extends du 5.2 y revient, dans un rôle inédit : la condition.And then there are the two spots where TypeScript looks away: mutable arrays, covariant for convenience where rigor would want them invariant; method parameters, bivariant by inheritance where strictFunctionTypes makes the others contravariant. These aren't bugs — they're chosen debts, a bit of soundness traded for a lot of ergonomics. So Vol 5 closes: a type travels (5.1), constrains to a capability (5.2), and crosses generics in a direction you can now read (5.3). Until now, types described and related values. The next volume crosses a threshold: computing a type from another — mapped types, conditional types, the language within the language. The same extends from 5.2 returns there, in a new role: the condition.