skip to content

Covariance, Contravariance & in/out Annotations

The producer/consumer intuition: output positions make a generic covariant, input positions make it contravariant, both together make it invariant. TypeScript normally measures this structurally, but 4.7 lets you state it explicitly with in and out.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

questions

4

In TypeScript with strict enabled, given `class Dog extends Animal`, `interface Box<T> { value: T }` and `interface Sink<T> { send: (value: T) => void }` — for each of Box and Sink, which instantiation is assignable to the other, and what rule decides the direction?

level: middleimportance: must knowfreq 50%

answer

  1. who produces and who consumes
  2. position of T inside the type
  3. a handler for any animal handles dogs
  4. output means same direction, input flips
  5. no keyword needed; measured from usage

basics

~10 s

Box<T> is covariant because T only appears in an output position, so Box<Dog> is assignable to Box<Animal>. Sink<T> is contravariant because T appears only as a parameter, so Sink<Animal> is assignable to Sink<Dog>.

solid answer

~50 s

The position where the type parameter appears decides the direction. In `Box<T>` the parameter is a *produced* value — you only ever read a `T` out — so the subtype relationship travels in the same direction: `Box<Dog>` is assignable to `Box<Animal>`, which is covariance. In `Sink<T>` the parameter is only *consumed*, sitting in a function parameter position, so the relationship reverses: `Sink<Animal>` is assignable to `Sink<Dog>`, which is contravariance. The intuition is that a sink that can handle any animal can safely stand in wherever something that handles dogs is expected, but a dog-only sink cannot stand in for an animal sink — it would be handed a cat. TypeScript does not require a keyword for this: it measures variance from how the parameter is used inside the type, and the whole check is compile-time only.

go deeper

for a junior

Be able to say that Dog being an Animal does not automatically make a wrapper of Dog usable as a wrapper of Animal, and that where the type parameter appears inside the type is what decides it.

for a middle

Explain the mechanics: read-only positions give covariance, parameter positions give contravariance, and TypeScript derives this from usage rather than from a keyword. Be ready to work an example out loud in both directions.

for a senior

Show the substitution reasoning rather than the label — argue from what a caller is entitled to do with the value. Point out that adding one consuming member to a shared type flips its variance and breaks callers who had no other change.

for a principal

Own the API consequence: variance is part of your public contract, so decide deliberately whether a shared generic reads, writes, or both, and know that the checker is predicting a hazard it cannot enforce at runtime.

## What variance is actually asking Variance is a question about *relationships travelling through a wrapper*. Start with two types where one is assignable to the other — say `Dog` is assignable to `Animal` because it has everything `Animal` has plus more. Now wrap both in the same generic: does `Box<Dog>` still relate to `Box<Animal>`, and in which direction? There are four possible answers, and they have names: - **Covariant** — the relationship survives unchanged: `Box<Dog>` is assignable to `Box<Animal>`. - **Contravariant** — the relationship flips: `Sink<Animal>` is assignable to `Sink<Dog>`. - **Invariant** — neither direction is allowed. - **Bivariant** — both directions are allowed (which is unsound in general, and TypeScript permits it only in specific places). ## Output positions produce covariance ```ts interface Box<T> { value: T } declare const boxOfDog: Box<Dog>; const boxOfAnimal: Box<Animal> = boxOfDog; // OK ``` Here `T` only ever comes *out* of the type. Whoever holds a `Box<Animal>` can only read `value` and treat it as an `Animal`; if the object underneath is really a `Box<Dog>`, every read yields a `Dog`, and a `Dog` is an `Animal`. Nothing can go wrong, so the assignment is allowed. Structurally the checker just compares the members: `value: Dog` against `value: Animal`, and `Dog` is assignable to `Animal`. The reverse fails for the same reason in mirror image: from a `Box<Animal>` you may only conclude that `value` is an `Animal`, which is not enough to satisfy a `Box<Dog>`. ## Input positions produce contravariance ```ts interface Sink<T> { send: (value: T) => void } declare const animalSink: Sink<Animal>; const dogSink: Sink<Dog> = animalSink; // OK ``` This is the direction people find backwards, and the fix is to stop thinking about the data and start thinking about the *obligation*. A `Sink<Dog>` promises: "give me dogs and I will cope." Does `Sink<Animal>` keep that promise? Yes — it copes with any animal, dogs included. So it can substitute. Does a `Sink<Dog>` satisfy a `Sink<Animal>` requirement? No — the holder is entitled to send a cat, and the dog sink was never written to handle one. Under `strict` (specifically `strictFunctionTypes`) parameter positions of function-typed *properties* like `send` above are checked in this contravariant direction. ## The producer/consumer mnemonic When `T` is **produced** — returned from a method, read from a property, yielded — the type is covariant, which is why the explicit annotation for it is spelled `out`. When `T` is **consumed** — accepted as a parameter — the type is contravariant, spelled `in`. That mnemonic is worth memorising because it survives every example you will be asked about. ## TypeScript measures variance structurally Unlike languages where you must annotate a parameter as covariant at its declaration, TypeScript by default derives variance from where the parameter occurs inside the type, and its answer matches what a plain member-by-member structural comparison would give. That has a practical consequence: adding one method that takes a `T` silently turns a covariant type into an invariant one, and callers who were happily passing `Container<Dog>` where `Container<Animal>` was expected start getting errors. Nothing in the type's name changed; the variance did. ## Nesting flips the direction twice Variance composes. If `T` sits in a parameter of a function that is itself a parameter, it flips twice and comes back to covariant: ```ts interface Source<T> { subscribe: (onValue: (value: T) => void) => void } ``` `Source<Dog>` is assignable to `Source<Animal>`? No — work it through: `onValue` is contravariant in its own parameter, and `subscribe` is contravariant in `onValue`, so `T` ends up covariant, and `Source<Dog>` *is* assignable to `Source<Animal>`. The same double flip is why `Promise<Dog>` is assignable to `Promise<Animal>` even though `T` appears inside callback parameters of `then`. ## Return positions are covariant Function types themselves follow the same rule: parameters contravariant, return type covariant. `() => Dog` is assignable to `() => Animal`, because a caller expecting an `Animal` back is perfectly served by receiving a `Dog`. ## It is all erased None of this exists at runtime. Variance is a rule the checker applies while comparing two type shapes; the emitted JavaScript contains no boxes, no sinks, and no type arguments. When variance rejects an assignment it is predicting a hazard, not preventing one — which is why an `as` cast silences it without making the program any safer.

  • Where do return types fit — is `() => Dog` assignable to `() => Animal`?
    Yes. Return positions are output positions, so function types are covariant in their return type: a caller expecting an `Animal` back is satisfied by a function that always returns a `Dog`. Combined with contravariant parameters, this gives the standard rule that a function is assignable when it accepts at least as much and returns at most as much as the target signature.
  • Why is `Promise<Dog>` assignable to `Promise<Animal>` when `T` appears inside `then`'s callback parameters?
    Because the flips cancel. `T` sits in a parameter of `onfulfilled`, which is itself a parameter of `then` — two contravariant positions compose back to covariance. The practical reading is that a promise only ever hands you a value, so it behaves like a producer and stays covariant.
  • Does TypeScript require you to declare variance at the type parameter, the way some languages do?
    No. By default the checker derives it from where the parameter is used, and the result matches a structural member-by-member comparison. Optional `in`/`out` annotations exist to state the variance explicitly, but they document and verify what the usage already implies rather than being required.

saying these in an interview costs you the question

  • Says all generics are covariant, like arrays
  • Thinks Sink<Dog> works where Sink<Animal> is required
  • Claims variance is checked when the object is created
  • Believes TypeScript needs a keyword to make a type covariant
  • Confuses covariance of the wrapper with subtyping of T itself

context

open as a page

In TypeScript with strict enabled, given `class Dog extends Animal` and `interface Cell<T> { value: T; set: (next: T) => void }`, is `Cell<Dog>` assignable to `Cell<Animal>`, is `Cell<Animal>` assignable to `Cell<Dog>`, or neither — and why?

level: middleimportance: should knowfreq 40%

basics

~10 s

Neither direction is allowed: Cell<T> is invariant because T appears in both an output position (value) and an input position (set). Covariance and contravariance both apply, and only the same type argument satisfies both.

open as a page

What do the `in`, `out` and `in out` variance annotations on a TypeScript generic type parameter do, and why add them when the compiler already works variance out from usage?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

They state a type parameter's variance explicitly: out means covariant, in means contravariant, in out means invariant. TypeScript verifies the declaration against how the parameter is used, and can then relate two instantiations by their type arguments instead of comparing members.

open as a page

A shared `Store<T>` type in your codebase both reads and writes `T`, so it is invariant and teams keep hitting errors passing a `Store<Dog>` to code that wants a `Store<Animal>`. How do you weigh splitting it into read and write views, adding variance annotations, and letting callers cast?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Split the type: give the reading half a parameter that appears only in output positions and the writing half one that appears only in input positions, then have APIs ask for the view they use. Variance annotations only assert existing variance, and casts hide the hazard.

open as a page