| |

TypeScript 41 🔷 Covariance, Contravariance, and Bivariance

Variance is the rule that decides whether a type A can be used where a type B is expected when A and B are related by subtyping. It sounds academic until you hit it in practice: you write a function that accepts a callback, pass a callback with a narrower parameter, and TypeScript rejects it. Or you assign a Dog[] to an Animal[], mutate it, and discover at runtime that a Cat ended up in your dog array. Variance is the theory underneath those errors, and once you understand it, the compiler’s messages stop looking arbitrary. This chapter covers the three variance kinds — covariant, contravariant, and bivariant — how they arise from read and write positions, how TypeScript applies them to functions, arrays, and generics, and how the strictFunctionTypes flag changes the rules. It also covers the related idea of in and out variance annotations on generic type parameters, which TypeScript added to let you state variance explicitly and catch mistakes earlier.

Key point: A position is covariant if it only produces values (a return type, a readonly property, an array element you only read). A position is contravariant if it only consumes values (a function parameter). A position is invariant if it both produces and consumes (a mutable property, a read-write array). TypeScript is covariant for return types, contravariant for function parameters when strictFunctionTypes is on, and historically bivariant for method parameters. Arrays are covariant in TypeScript even though this is unsound, for practical reasons. The in and out modifiers on type parameters let you declare variance explicitly.


What variance is, and why it exists

Subtyping asks: when is type A usable where type B is expected? Dog is a subtype of Animal because every Dog is an Animal. That much is obvious. Variance asks the harder question: when is a container or function over A usable where one over B is expected? Is Dog[] usable where Animal[] is expected? Is (d: Dog) => void usable where (a: Animal) => void is expected? The answers are not the same, and the differences are what variance names.

Read and write positions. The rule of thumb is: a value that flows out of a structure is covariant, and a value that flows into a structure is contravariant. If Animal[] gives you Animals when you read, then anything that gives you a Dog when you read is a valid substitute — Dog[] works, because a Dog is an Animal. But if Animal[] lets you put any Animal in, then a Dog[] is not a safe substitute, because someone could put a Cat in. That is the array problem, and it is why mutable arrays are unsound in TypeScript.

Why variance is hard to see in small code. With one function and one call, variance does not come up. It appears when you assign functions to variables, pass callbacks to higher-order functions, store objects in arrays, or write generic containers. The moment two function types have to be compared — is this callback acceptable where that callback is expected? — variance is the deciding rule.

Why TypeScript is not fully sound here. A sound type system would make mutable arrays invariant and reject Dog[] where Animal[] is expected. TypeScript instead made arrays covariant because the ergonomic cost of invariance was too high: Animal[] is how almost everyone writes array parameters, and forcing readonly Animal[] or generics everywhere would break enormous amounts of real code. The tradeoff is deliberate — convenience over provable soundness — and it is why you can still get a runtime surprise by pushing a Cat into a Dog[] that was passed as Animal[].

Why the terms are worth learning. The words “covariant,” “contravariant,” and “bivariant” are the vocabulary TypeScript’s error messages assume you have, especially around strictFunctionTypes. Without them, the difference between “this function’s parameter is too wide” and “this function’s return type is too narrow” looks like noise. With them, the compiler’s complaints become readable.


Covariance: producing values

Covariance is the intuitive case. If Dog is a subtype of Animal, then a producer of Dog is a subtype of a producer of Animal. Reading is the classic covariant position.

interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

// Covariant: a function that returns Dog is usable where one returning Animal is expected
let getAnimal: () => Animal = () => ({ name: "generic" });
let getDog: () => Dog = () => ({ name: "Rex", breed: "Lab" });

getAnimal = getDog; // ✅ Dog is an Animal, so returning Dog is fine

getDog returns a Dog, and a Dog is an Animal, so assigning it to getAnimal is safe. The return position is covariant. This is the case everyone finds natural.

// Readonly properties are covariant
interface ReadonlyBox<T> {
  readonly value: T;
}

let animalBox: ReadonlyBox<Animal> = { value: { name: "generic" } };
let dogBox: ReadonlyBox<Dog> = { value: { name: "Rex", breed: "Lab" } };

animalBox = dogBox; // ✅ readonly position is covariant

Because value is readonly, it can only be read. Reading is covariant, so ReadonlyBox<Dog> is assignable to ReadonlyBox<Animal>. The same logic applies to readonly array types:

let animals: readonly Animal[] = [];
let dogs: readonly Dog[] = [{ name: "Rex", breed: "Lab" }];

animals = dogs; // ✅ readonly arrays are covariant

readonly Dog[] cannot be mutated, so the “someone pushes a Cat” problem does not exist. Covariance is sound when there is no write.

Why covariance is the default intuition. People expect subtype relationships to carry through. If Dog is an Animal, then a Dog box should be an Animal box. That intuition is correct exactly when the box is read-only. The moment writing enters, the intuition breaks, and that is the next section.


Contravariance: consuming values

Contravariance is the counterintuitive case, and it is where most people’s mental model fails. If Dog is a subtype of Animal, then a consumer of Animal is a subtype of a consumer of Dog — the relationship flips.

// Contravariant: a function that accepts Animal is usable where one accepting Dog is expected
let handleDog: (dog: Dog) => void;
let handleAnimal: (animal: Animal) => void = (a) => console.log(a.name);

handleDog = handleAnimal; // ✅ Animal handler can handle a Dog

handleAnimal promises to accept any Animal. A Dog is an Animal, so handleAnimal can handle a Dog. Therefore handleAnimal is a valid substitute for handleDog. The parameter position is contravariant: the wider the accepted type, the more places the function can be used.

let handleSpecific: (dog: Dog) => void = (d) => console.log(d.breed);

// handleAnimal = handleSpecific; // ❌ with strictFunctionTypes

handleSpecific only accepts a Dog. If it were assigned to handleAnimal, someone could call it with a Cat, and handleSpecific would try to read .breed on a Cat. The compiler rejects this when strictFunctionTypes is enabled.

Why the direction flips. A value that flows into a function is a requirement the function places on its caller. The more a function requires, the fewer callers can satisfy it. So a function accepting Dog has a stronger requirement than one accepting Animal, which makes it less substitutable, not more. Contravariance says: narrower inputs make a function less general, so the subtype relationship reverses.

Method parameters are the historical exception. TypeScript treats parameters declared with method syntax as bivariant, even under strictFunctionTypes. This was a deliberate concession to make class and interface inheritance ergonomic.

interface Handler {
  handle(animal: Animal): void; // method syntax — bivariant
}

interface SpecificHandler {
  handle(dog: Dog): void;
}

let h: Handler = {} as SpecificHandler; // ✅ allowed, bivariant

The same comparison written with a function property type would be rejected under strictFunctionTypes. The asymmetry is intentional and is one of the most surprising corners of the language — a method and an equivalent function property do not behave the same way.

Why method bivariance exists. Making method parameters strictly contravariant broke too much real-world code, especially around event handlers and library interfaces where narrower parameters are common. Rather than force every library to rewrite, TypeScript kept the permissive behavior for methods and applied strictness only to function-typed properties. The practical result: if you want strictness, declare your callable members as properties, not methods.


Bivariance and the strictFunctionTypes flag

Bivariance means a position accepts both directions — a Dog-parameter function and an Animal-parameter function are treated as mutually assignable. Before TypeScript 2.6, all function parameters were bivariant. That was unsound, but it matched JavaScript’s loose behavior and let a lot of code compile. strictFunctionTypes (part of strict) turns parameters contravariant for function type positions, leaving method positions bivariant.

// With strictFunctionTypes: false (or before 2.6)
type Handler = (animal: Animal) => void;
type SpecificHandler = (dog: Dog) => void;

let h: Handler = (a) => {};
let s: SpecificHandler = (d) => {};

h = s; // ✅ bivariant, both directions allowed
s = h; // ✅

// With strictFunctionTypes: true
h = s; // ❌ contravariant, SpecificHandler is too narrow
s = h; // ✅ wider parameter is assignable to narrower

The flag changes which assignments are legal, so turning it on can produce new errors in existing code. Those errors are usually correct — they identify real unsoundness — but they can also appear in library code you do not control, which is one reason method parameters were left bivariant.

Why the flag matters for correctness. Bivariant parameters let you write code that type-checks but crashes at runtime. The classic case: a callback typed (e: Event) => void assigned a function that assumes a MouseEvent, then invoked with a KeyboardEvent. With contravariant parameters, TypeScript forces the callback to accept the wider type or narrow explicitly. That catches a whole class of event-handler bugs.

Why the default still permits some unsoundness. Between method bivariance, array covariance, and any, TypeScript is not fully sound. The design goal is to be useful first: catch the errors people actually make, without rejecting the patterns people actually write. strictFunctionTypes narrows the unsoundness but does not eliminate it.


Variance annotations: in and out

TypeScript 4.7 added the ability to declare variance on generic type parameters explicitly. out T marks a covariant parameter, in T marks a contravariant one, and in out T marks an invariant one. These are checked against the actual use of T in the type’s body.

interface Producer<out T> {
  produce(): T;
}

interface Consumer<in T> {
  consume(value: T): void;
}

interface Both<in out T> {
  get(): T;
  set(value: T): void;
}

Producer<Dog> is assignable to Producer<Animal> because out T declares covariance and the body only uses T in the return position. Consumer<Animal> is assignable to Consumer<Dog> because in T declares contravariance and T only appears in the parameter position. Both<T> is invariant, so no substitution is permitted.

Why annotations are useful. Without them, the compiler infers variance structurally, which means a mistake in the body can silently produce the wrong variance. With out T declared, if you accidentally use T in an input position, the compiler reports an error at the declaration. Annotations make the intended variance part of the type’s contract rather than an emergent property.

interface BadProducer<out T> {
  produce(): T;
  consume(value: T): void; // ❌ error: T is used in an input position
}

The error is precise: out T promises T is only produced, and consume violates that. This catches the mistake at the definition, not at every assignment site.

Why annotations also help performance. Once variance is known, the compiler can skip re-computing structural relationships in deep generic types. For large codebases, explicit variance is both correctness and speed.

Why annotations are optional and why that is fine. The compiler infers variance correctly in the overwhelming majority of cases, so most code never needs in or out. The annotations are a tool for library authors who want the contract stated and checked, and for cases where inference produces something surprising. For application code, structural inference is usually enough.


Arrays: covariant and unsound

TypeScript treats Array<T> as covariant in T, which is unsound for mutable arrays. This is the single most common place where variance bites in practice.

let animals: Animal[] = [];
let dogs: Dog[] = [{ name: "Rex", breed: "Lab" }];

animals = dogs; // ✅ allowed (covariant, unsound)

animals.push({ name: "Whiskers" }); // a Cat, or just an Animal

// Now dogs[1] exists but is not a Dog — runtime surprise
const d = dogs[1];
console.log(d.breed); // undefined at runtime, but TypeScript says string

The assignment compiles, the push compiles, and the read of breed compiles — but the value was never a Dog. This is the array covariance hole.

The fix: readonly T[]. A readonly array cannot be pushed into, so covariance is sound there.

let animals: readonly Animal[] = [];
let dogs: readonly Dog[] = [{ name: "Rex", breed: "Lab" }];

animals = dogs; // ✅ sound, no mutation possible

Why TypeScript made this choice. Making arrays invariant would require Animal[] parameters to be written as readonly Animal[] or generic over a read-only interface, which would break compatibility with an enormous amount of JavaScript and library code. The maintainers judged the ergonomic cost too high and accepted the unsoundness, documenting it as a known tradeoff. Knowing where the hole is lets you avoid it deliberately.

Tuple types follow the same rule. A tuple is covariant in its element positions, with the same unsoundness if it is mutable.


Variance in practice: callbacks, events, and generics

The place variance appears most often is callbacks. A function that takes a callback is asking you to supply a function, and the parameter position of that callback is contravariant.

function onEvent(handler: (e: MouseEvent) => void): void {
  // implementation omitted
}

// With strictFunctionTypes:
onEvent((e: MouseEvent) => console.log(e.clientX)); // ✅ exact
onEvent((e: Event) => console.log(e.type));         // ✅ wider parameter, contravariant OK
onEvent((e: KeyboardEvent) => console.log(e.key));  // ❌ narrower parameter, rejected

The third call is rejected because the handler promises to accept only KeyboardEvent, but onEvent may call it with any MouseEvent. Contravariance is what makes the compiler catch this.

Generic containers are where the practical consequences compound. A container that both reads and writes its element is invariant, and that invariance propagates through every layer above it. This is why Array<Dog> is not assignable to Array<Animal> in languages with sound arrays, and why TypeScript’s choice to be covariant here is so consequential.

// A sound generic box is invariant in its mutable position
interface Box<T> {
  get(): T;      // covariant use
  set(v: T): void; // contravariant use
}

let animalBox: Box<Animal> = {} as Box<Animal>;
let dogBox: Box<Dog> = {} as Box<Dog>;

// animalBox = dogBox; // ❌ invariant — both read and write

Box both produces and consumes T, so it is invariant. The compiler infers this structurally, and no substitution in either direction is allowed. This is the correct behavior, and it is what readonly and separate producer/consumer interfaces are designed to let you relax when appropriate.


Complete Example Session

// ============================================
// PART 1: SETUP
// ============================================

interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

interface Cat extends Animal {
  lives: number;
}

// ============================================
// PART 2: COVARIANT RETURN POSITION
// ============================================

let getAnimal: () => Animal = () => ({ name: "generic" });
let getDog: () => Dog = () => ({ name: "Rex", breed: "Lab" });

getAnimal = getDog; // ✅ Dog is an Animal
// getDog = getAnimal; // ❌ Animal is not necessarily a Dog

// ============================================
// PART 3: CONTRAVARIANT PARAMETER POSITION
// ============================================

let handleDog: (dog: Dog) => void = (d) => console.log(d.breed);
let handleAnimal: (animal: Animal) => void = (a) => console.log(a.name);

handleDog = handleAnimal; // ✅ Animal handler can handle Dog
// handleAnimal = handleDog; // ❌ under strictFunctionTypes

// ============================================
// PART 4: METHOD BIVARIANCE
// ============================================

interface Handler {
  handle(animal: Animal): void;
}

interface SpecificHandler {
  handle(dog: Dog): void;
}

let handler: Handler = {} as SpecificHandler; // ✅ method syntax stays bivariant

// ============================================
// PART 5: ARRAY COVARIANCE (UNSOUND)
// ============================================

let animals: Animal[] = [];
let dogs: Dog[] = [{ name: "Rex", breed: "Lab" }];

animals = dogs; // ✅ allowed (unsound)
animals.push({ name: "Whiskers" }); // a plain Animal
// dogs[1] is typed Dog but is actually not — runtime surprise

// ============================================
// PART 6: READONLY ARRAY COVARIANCE (SOUND)
// ============================================

let roAnimals: readonly Animal[] = [];
let roDogs: readonly Dog[] = [{ name: "Rex", breed: "Lab" }];

roAnimals = roDogs; // ✅ sound, cannot push

// ============================================
// PART 7: INVARIANCE IN A MUTABLE BOX
// ============================================

interface Box<T> {
  get(): T;
  set(v: T): void;
}

let animalBox: Box<Animal> = {} as Box<Animal>;
let dogBox: Box<Dog> = {} as Box<Dog>;

// animalBox = dogBox; // ❌ invariant

// ============================================
// PART 8: EXPLICIT VARIANCE ANNOTATIONS
// ============================================

interface Producer<out T> {
  produce(): T;
}

interface Consumer<in T> {
  consume(value: T): void;
}

let animalProducer: Producer<Animal> = {} as Producer<Animal>;
let dogProducer: Producer<Dog> = {} as Producer<Dog>;

animalProducer = dogProducer; // ✅ out T is covariant

let animalConsumer: Consumer<Animal> = {} as Consumer<Animal>;
let dogConsumer: Consumer<Dog> = {} as Consumer<Dog>;

dogConsumer = animalConsumer; // ✅ in T is contravariant

// ============================================
// PART 9: ANNOTATION CATCHES MISUSE
// ============================================

interface BadProducer<out T> {
  produce(): T;
  // consume(value: T): void; // ❌ error: T in input position
}

// ============================================
// PART 10: CALLBACK CONTRAVARIANCE IN PRACTICE
// ============================================

function onEvent(handler: (e: MouseEvent) => void): void {
  // implementation omitted
}

onEvent((e: MouseEvent) => console.log(e.clientX)); // ✅
onEvent((e: Event) => console.log(e.type));         // ✅ wider
// onEvent((e: KeyboardEvent) => console.log(e.key)); // ❌ narrower

Each part isolates one variance case. Parts 2 and 3 show the two directions, part 4 shows the method exception, parts 5 and 6 show the array soundness split, and parts 7 through 10 show invariance, annotations, and a real callback scenario.


Quick Reference

Variance Kinds

KindDirectionTypical Position
CovariantA <: B → F<A> <: F<B>Return type, readonly property
ContravariantA <: B → F<B> <: F<A>Function parameter
InvariantNeither directionMutable property, read-write
BivariantBoth directionsMethod parameters (TypeScript legacy)

Position Rules

PositionVarianceSound?
() => TCovariant in T✅
(x: T) => voidContravariant in T✅ under strictFunctionTypes
readonly TCovariant in T✅
T (mutable property)Invariant in T✅
T[] (mutable)Covariant in T❌ unsound
readonly T[]Covariant in T✅
Method m(x: T)Bivariant in T❌ unsound

Variance Annotations

AnnotationMeaningChecked
out TCovariantT only in output positions
in TContravariantT only in input positions
in out TInvariantT in both positions

strictFunctionTypes

SettingParameter Behavior
falseBivariant (both directions allowed)
trueContravariant (wider only)
Method syntaxBivariant regardless
Function property syntaxFollows the flag

Assignability Cheat Sheet

FromToAllowed?
() => Dog() => Animal✅ covariant
() => Animal() => Dog❌
(a: Animal) => void(d: Dog) => void✅ contravariant
(d: Dog) => void(a: Animal) => void❌ under strict
Dog[]Animal[]✅ unsound
readonly Dog[]readonly Animal[]✅ sound
Box<Dog>Box<Animal>❌ invariant

Best Practices

✅ Do This:

// Enable strictFunctionTypes (part of strict)
// tsconfig.json: "strict": true                       // ✅

// Use readonly arrays for covariant parameters
function sum(animals: readonly Animal[]): number { return animals.length; } // ✅

// Declare variance explicitly on library generics
interface Producer<out T> { produce(): T; }           // ✅
interface Consumer<in T> { consume(v: T): void; }     // ✅

// Prefer function property syntax when strictness matters
type Handler = { handle: (a: Animal) => void };       // ✅ contravariant

// Model producers and consumers separately
interface Readable<out T> { get(): T; }
interface Writable<in T> { set(v: T): void; }         // ✅

❌ Don’t Do This:

// Don't pass mutable arrays where a narrower type is expected
let a: Animal[] = [] as Dog[];                        // ⚠️ unsound

// Don't rely on method bivariance for safety
interface H { handle(d: Dog): void }                  // ⚠️ bivariant

// Don't annotate out T and then use T as input
interface Bad<out T> { consume(v: T): void }          // ⚠️ error

// Don't assume Dog[] -> Animal[] is safe because it compiles
// It is covariant but unsound                            // ⚠️

// Don't mix method and property syntax casually
// They behave differently under strictFunctionTypes      // ⚠️

Common Pitfalls

PitfallProblemSolution
Narrower callback parameterRejected under strictFunctionTypesWiden the parameter
Array covariance surpriseNon-Dog value in a Dog[]Use readonly arrays
Method bivarianceUnsound assignments compileUse property syntax
Wrong in/out annotationError at declarationMatch annotation to use
Box<T> assumed covariantInvariant, rejectedSplit into producer/consumer
strictFunctionTypes offUnsound code acceptedEnable strict
Mixing in outNo substitution allowedUse explicit producer/consumer types
Expecting soundnessTypeScript is intentionally unsoundKnow the holes

Real-World Examples

1. Event handler too narrow

element.addEventListener("click", (e: MouseEvent) => {}); // ✅

2. Wider event handler

function listen(h: (e: Event) => void): void {}
listen((e: MouseEvent) => {}); // ✅ contravariant

3. Array parameter unsoundness

function addAnimal(list: Animal[]): void {
  list.push({ name: "new" });
}
const dogs: Dog[] = [];
addAnimal(dogs); // compiles, unsound

4. Readonly array parameter

function count(list: readonly Animal[]): number {
  return list.length;
}
count(dogs); // ✅ sound

5. Producer variance

interface Factory<out T> {
  create(): T;
}
let f: Factory<Animal> = {} as Factory<Dog>; // ✅

6. Consumer variance

interface Sink<in T> {
  accept(v: T): void;
}
let s: Sink<Dog> = {} as Sink<Animal>; // ✅

7. Invariant box

interface Box<T> { get(): T; set(v: T): void; }
// Box<Dog> and Box<Animal> are mutually unassignable

8. Method vs property

interface A { m(x: Animal): void }      // bivariant
interface B { m: (x: Animal) => void }  // contravariant

9. Explicit variance catches bugs

interface Source<out T> {
  next(): T;
  // reset(v: T): void; // would error — T in input position
}

10. Generic function parameter

function map<T, U>(arr: readonly T[], f: (t: T) => U): U[] {
  return arr.map(f);
}

Visual: Covariance vs Contravariance

┌──────────────────────────────────────────────────────┐
│  Dog  <:  Animal                                     │
│                                                      │
│  COVARIANT (output):                                 │
│    Producer<Dog>  <:  Producer<Animal>               │
│                                                      │
│    () => Dog   is assignable to   () => Animal       │
│                                                      │
│    A Dog is an Animal, so producing one is fine.     │
│                                                      │
│  CONTRAVARIANT (input):                              │
│    Consumer<Animal>  <:  Consumer<Dog>               │
│                                                      │
│    (a: Animal) => void  is assignable to             │
│    (d: Dog) => void                                  │
│                                                      │
│    Handling any Animal means you can handle a Dog.   │
│                                                      │
└──────────────────────────────────────────────────────┘

Visual: Read/Write Positions

┌──────────────────────────────────────────────────────┐
│                                                      │
│    Value flows OUT  ──►  Covariant  (return, read)   │
│                                                      │
│    Value flows IN   ──►  Contravariant (parameter)   │
│                                                      │
│    Value flows BOTH ──►  Invariant  (mutable prop)   │
│                                                      │
│  ┌─────────────────────────────────────────────┐     │
│  │  interface Box<T> {                         │     │
│  │    get(): T;        ← T flows out (covar)   │     │
│  │    set(v: T): void; ← T flows in  (contra)  │     │
│  │  }                                          │     │
│  │                                             │     │
│  │  Both → invariant                           │     │
│  └─────────────────────────────────────────────┘     │
│                                                      │
└──────────────────────────────────────────────────────┘

Visual: Array Covariance Hole

┌──────────────────────────────────────────────────────┐
│  let animals: Animal[] = [];                         │
│  let dogs: Dog[] = [{ name: "Rex", breed: "Lab" }];  │
│                                                      │
│  animals = dogs;        // ✅ compiles                │
│                                                      │
│  animals.push({ name: "Whiskers" });                 │
│                                                      │
│  Now dogs[1] exists.                                 │
│  TypeScript says it is a Dog.                        │
│  At runtime it is a plain Animal (no breed).         │
│                                                      │
│  ┌────────────────────────────────────────┐          │
│  │  dogs[1].breed  →  undefined at runtime │          │
│  │  but typed as string by the compiler    │          │
│  └────────────────────────────────────────┘          │
│                                                      │
│  Fix: use readonly Animal[]                          │
│                                                      │
└──────────────────────────────────────────────────────┘

Visual: strictFunctionTypes Effect

┌──────────────────────────────────────────────────────┐
│  type Wide = (a: Animal) => void                     │
│  type Narrow = (d: Dog) => void                      │
│                                                      │
│  strictFunctionTypes: false (bivariant)              │
│    Wide = Narrow   ✅                                │
│    Narrow = Wide   ✅                                │
│                                                      │
│  strictFunctionTypes: true (contravariant)           │
│    Wide = Narrow   ❌  Narrow is too specific        │
│    Narrow = Wide   ✅  Wide accepts more             │
│                                                      │
│  Method syntax: bivariant regardless                 │
│    interface H { m(x: Animal): void }                │
│                                                      │
└──────────────────────────────────────────────────────┘

Visual: in / out Annotations

┌──────────────────────────────────────────────────────┐
│                                                      │
│  interface Producer<out T> { produce(): T; }         │
│                     ▲                                │
│                     └── T only in output             │
│                                                      │
│  interface Consumer<in T> { consume(v: T): void; }   │
│                     ▲                                │
│                     └── T only in input              │
│                                                      │
│  interface Both<in out T> {                          │
│    get(): T;                                         │
│    set(v: T): void;                                  │
│  }                                                   │
│                                                      │
│  Compiler checks: out T used as input → error        │
│                                                      │
└──────────────────────────────────────────────────────┘

Summary

ConceptRuleExample
CovarianceA <: B → F<A> <: F<B>Return type, readonly
ContravarianceA <: B → F<B> <: F<A>Function parameter
InvarianceNo substitutionMutable property
BivarianceBoth directionsMethod parameters
out TDeclares covariantProducer
in TDeclares contravariantConsumer
in out TDeclares invariantRead-write
strictFunctionTypesMakes parameters contravariantPart of strict
Mutable arrayCovariant, unsoundDog[] → Animal[]
readonly arrayCovariant, soundSafe

Key takeaways:

  • Variance describes substitutability of parameterized types — whether F<A> can replace F<B> when A and B are related
  • Output positions are covariant, input positions are contravariant, both is invariant — the read/write rule is the whole intuition
  • Function return types are covariant, function parameters are contravariant when strictFunctionTypes is on
  • Method parameters stay bivariant in TypeScript, a deliberate unsoundness for ergonomics
  • Arrays are covariant and unsound — Dog[] is assignable to Animal[], and a push can break the invariant; readonly arrays are sound
  • in and out annotations state variance explicitly and let the compiler check the declaration rather than infer it
  • Variance errors are the compiler catching real bugs, not arbitrary rules — the callback that assumes a narrower event type really would crash
  • TypeScript prioritizes usability over soundness — knowing where the holes are lets you avoid them deliberately

Remember: Variance is not trivia. It is the reason a callback with the wrong parameter type is rejected, the reason readonly changes what assignments are legal, and the reason a mutable array can hold a value its type says it cannot. Learn the read/write rule first: output is covariant, input is contravariant, both is invariant. Everything else follows from it.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!