| |

TypeScript 39 ๐Ÿ”ท Variance and Type Compatibility

Variance describes how subtype relationships between types propagate through containers โ€” arrays, functions, promises, and generics. If Dog is a subtype of Animal, is Dog[] a subtype of Animal[]? Is () => Dog a subtype of () => Animal? Is (x: Animal) => void a subtype of (x: Dog) => void? The answers depend on direction โ€” covariant, contravariant, invariant, or bivariant. This is one of the most conceptually dense parts of TypeScript, but it explains why some assignments work and others don’t, why strictFunctionTypes matters, and where the compiler’s soundness has deliberate holes.

Key point: Variance answers: if A is assignable to B, is F<A> assignable to F<B>? Covariant means yes (same direction). Contravariant means yes in the opposite direction. Invariant means no โ€” the types must match exactly. Bivariant means yes in either direction. Arrays are covariant (unsoundly). Function returns are covariant. Function parameters are contravariant under strictFunctionTypes. Method parameters are bivariant. All of this flows from assignability โ€” the same rule TypeScript uses everywhere else.


What variance is

Variance is how subtype relationships transform through a type constructor.

Given:

  • A is a subtype of B
  • F is a type constructor (like Array, Promise, () => _)

What is the relationship between F<A> and F<B>?

VarianceRelationship
CovariantF<A> is a subtype of F<B>
ContravariantF<B> is a subtype of F<A>
InvariantNeither โ€” F<A> and F<B> are unrelated
BivariantBoth โ€” either direction works

In TypeScript terms: Subtype means assignable. A assignable to B means “every A can be used where a B is expected.”

Why it matters: Variance explains the rules for arrays, functions, and generics. It’s why Dog[] is assignable to Animal[] (covariant), why a function that takes Animal is assignable to one that takes Dog (contravariant), and why Promise<Dog> is assignable to Promise<Animal> (covariant).

Where variance appears:

ContextVariance of the type parameter
Array elementsCovariant
Function returnCovariant
Function parametersContravariant
Method parametersBivariant
Promise<T>Covariant
ReadonlyArray<T>Covariant
Map<K, V>Invariant in K, covariant in V

Why “variance”: The subtype relationship “varies” as it passes through the container. It may stay the same (covariant), flip (contravariant), or break (invariant). The term describes the direction of the variation.

Why this is hard: Variance isn’t a rule the compiler imposes โ€” it’s a consequence of assignability. Once you know what “assignable” means, variance follows. The difficulty is that assignability is defined recursively, so variance is too.


Covariance โ€” same direction

Covariance: if A is assignable to B, then F<A> is assignable to F<B>.

Arrays are covariant:

class Animal {
  name = '';
}

class Dog extends Animal {
  breed = '';
}

let animals: Animal[] = [];
let dogs: Dog[] = [];

animals = dogs;   // โœ… Dog[] assignable to Animal[]

Every Dog is an Animal. An array of Dogs is an array of Animals. The subtype relationship stays the same direction.

Function returns are covariant:

type Fn = () => Animal;

const returnsAnimal: () => Animal = () => ({ name: 'a' });
const returnsDog: () => Dog = () => ({ name: 'd', breed: 'lab' });

const fn: Fn = returnsDog;   // โœ…

A function returning Dog can be used where a function returning Animal is expected. The return type stays in the same direction.

Promise<T> is covariant:

let pa: Promise<Animal> = Promise.resolve({ name: 'a' });
let pd: Promise<Dog> = Promise.resolve({ name: 'd', breed: 'lab' });

pa = pd;   // โœ…

Why covariance for returns and arrays: The output position โ€” what a function returns, what an array holds โ€” follows the same direction. A Dog is an Animal, so a container of Dogs is a container of Animals.

The array exception: Arrays are also mutable, which makes covariance unsound.

const dogs: Dog[] = [];
const animals: Animal[] = dogs;

animals.push({ name: 'generic animal' });   // โš ๏ธ  no error!
// dogs now contains a plain Animal, not a Dog

TypeScript allows this โ€” it’s a known unsoundness. Arrays are covariant because banning it would break too much real code.

The sound alternative โ€” readonly:

const dogs: readonly Dog[] = [];
const animals: readonly Animal[] = dogs;   // โœ… sound
// animals.push(...)  // โŒ readonly prevents it

readonly Dog[] is covariant and sound because no mutation can happen.

Why arrays are unsound: TypeScript’s designers chose pragmatism. Most code doesn’t mutate arrays in ways that break the type. Enforcing invariance would reject valid patterns. The unsoundness is documented and accepted.

Why covariance is the “natural” direction: A Dog is an Animal. A value that produces Dogs produces Animals. The relationship goes the same way. Covariance is what you’d naively expect. Contravariance is the surprise.


Contravariance โ€” opposite direction

Contravariance: if A is assignable to B, then F<B> is assignable to F<A>.

Function parameters are contravariant under strictFunctionTypes:

type HandleAnimal = (a: Animal) => void;
type HandleDog = (d: Dog) => void;

const handleAnimal: HandleAnimal = (a) => console.log(a.name);
const handleDog: HandleDog = (d) => console.log(d.breed);

const h1: HandleAnimal = handleDog;   // โŒ
const h2: HandleDog = handleAnimal;   // โœ…

Wait โ€” why is handleAnimal assignable to HandleDog but not the other way?

The reasoning: A function that handles any Animal can handle a Dog โ€” a Dog is an Animal. So HandleAnimal is assignable to HandleDog.

A function that handles only Dogs can’t handle any Animal โ€” what if you passed a Cat? So HandleDog is not assignable to HandleAnimal.

The direction flips: Animal โ†’ Dog is a narrowing, and the function type goes the other way.

In a call-site example:

function callHandler(handler: HandleDog): void {
  handler({ name: 'Rex', breed: 'lab' });   // passes a Dog
}

const animalHandler: HandleAnimal = (a) => console.log(a.name);

callHandler(animalHandler);   // โœ… โ€” animalHandler can handle any Animal

animalHandler accepts an Animal and gets a Dog. Since Dog is an Animal, it works.

Without strictFunctionTypes: Function parameters are bivariant โ€” both directions work. strictFunctionTypes tightens this to contravariance. That’s why you should enable it.

Why contravariance matters: It’s how function assignability works when the parameter types differ. Calling code passes specific values; the function must accept them. The requirement flows opposite to the subtype direction.

Why strictFunctionTypes exists: Before it, function parameters were bivariant โ€” a source of real bugs. A function expecting Dog could be assigned where Animal was expected, and calling code would pass a Cat. strictFunctionTypes catches it.

The method exception: Method parameters (defined with method syntax) are still bivariant even with strictFunctionTypes:

interface Handler {
  handle(a: Animal): void;    // method โ€” bivariant
}

interface HandlerFn {
  handle: (a: Animal) => void;  // function property โ€” contravariant
}

That’s a subtle difference between the two syntaxes. Use function-property syntax when you want contravariance; method syntax keeps bivariance for compatibility.

Why parameter direction flips: A function’s parameter is a requirement. The function says “I need at least this much.” A function requiring Animal needs less than one requiring Dog. So it’s more general โ€” assignable to more places. Contravariance captures that.


Bivariance โ€” both directions

Bivariance: F<A> and F<B> are assignable in either direction.

Method parameters are bivariant:

interface Store {
  add(item: Animal): void;   // method syntax
}

interface StoreFn {
  add: (item: Animal) => void;   // function property
}

With Store, add accepts either Animal or Dog handlers โ€” both directions work.

Why methods are bivariant: Compatibility with older code and libraries. Changing method parameters to contravariant broke too much. TypeScript kept them bivariant as a compromise.

Implications: Method parameters are less strict than function-property parameters. If you want contravariance, use the function-property syntax.

The difference in practice:

interface A {
  m(x: Dog): void;    // bivariant
}

interface B {
  m: (x: Dog) => void;  // contravariant
}

const a: A = { m: (x: Animal) => {} };  // โœ…
const b: B = { m: (x: Animal) => {} };  // โŒ under strictFunctionTypes

Why bivariance persists: Method syntax is common in interfaces. Making it strict would break too many real-world patterns. The compromise: function properties are contravariant, methods stay bivariant.

Why bivariance is a compromise: It’s not theoretically sound โ€” a HandleDog assignable to HandleAnimal means you could pass a Cat at runtime. But in practice, methods are called from the same class more often than assigned across types. The unsoundness is rare. TypeScript accepts it for compatibility.


Invariance โ€” neither direction

Invariance: F<A> and F<B> are unrelated โ€” neither is assignable to the other.

Mutable properties are invariant:

interface Box<T> {
  value: T;
}

let animalBox: Box<Animal> = { value: { name: 'a' } };
let dogBox: Box<Dog> = { value: { name: 'd', breed: 'lab' } };

animalBox = dogBox;   // โœ…?
dogBox = animalBox;   // โŒ

Hmm โ€” Box<Dog> is assignable to Box<Animal> in TypeScript, because Box is structurally covariant when the property is read-only? No โ€” actually, mutable properties are covariant in TypeScript, not invariant. Let me correct.

What’s invariant in TypeScript:

Type parameters that are used in both input and output positions tend to be invariant in a stricter language. TypeScript is less strict โ€” it treats many generic types as covariant even when they’re mutable.

Example โ€” function types with the parameter and return both using T:

type Fn<T> = (arg: T) => T;

let animalFn: Fn<Animal> = a => a;
let dogFn: Fn<Dog> = d => d;

animalFn = dogFn;   // โŒ under strictFunctionTypes
dogFn = animalFn;   // โŒ

Fn<Dog> has Dog in the parameter (contravariant) and Dog in the return (covariant). The two directions cancel โ€” the result is invariant. Neither is assignable to the other.

Where invariance appears:

  • Types with a type parameter in both parameter and return positions
  • Types with mutable properties where reads and writes conflict
  • Map<K, V> โ€” the value type is invariant in TypeScript? Actually covariant.

TypeScript’s pragmatism: Full invariance would reject many useful patterns. TypeScript accepts some unsoundness in the name of ergonomics. It’s not as strict as languages like Scala or Kotlin in variance checking.

Why invariance is rare in TypeScript: TypeScript is structural and pragmatic. It treats most generic types as covariant for convenience. Invariance shows up mostly when variance direction cancels โ€” like the Fn<T> example.

Why some languages enforce invariance: Because mutation breaks covariance. If Box<Dog> is a subtype of Box<Animal>, and Box is mutable, you could put an Animal in the box and later read it as a Dog. Invariance prevents that.

Why TypeScript doesn’t: Because the strict version rejects too much real code. The compromise: covariant generics with documented unsoundness.

Why Fn<T> is invariant: T is used as both input and output. Input is contravariant; output is covariant. Two opposite directions with the same type parameter cancel. Neither Fn<Dog> nor Fn<Animal> is a subtype of the other.


strictFunctionTypes in detail

The compiler flag strictFunctionTypes (included in strict) enables contravariant function parameter checks.

With strictFunctionTypes: false:

type HandleAnimal = (a: Animal) => void;
type HandleDog = (d: Dog) => void;

const h1: HandleDog = (a: Animal) => {};   // โœ… bivariant
const h2: HandleAnimal = (d: Dog) => {};   // โœ… bivariant

Both directions are allowed.

With strictFunctionTypes: true:

const h1: HandleDog = (a: Animal) => {};   // โœ… contravariant โ€” allowed
const h2: HandleAnimal = (d: Dog) => {};   // โŒ contravariant โ€” rejected

Only the safe direction is allowed.

What strictFunctionTypes affects:

  • Function type assignability
  • Callback parameter types
  • Higher-order functions
  • Array.prototype.map callbacks
  • React event handlers

What it doesn’t affect:

  • Method parameters (still bivariant)
  • Class instance methods
  • Interface methods

Why enable it: It catches a real class of bugs โ€” passing a function that expects a narrower type where a broader type is expected. That’s how runtime crashes happen.

Example of the bug it catches:

type Comparator = (a: Animal, b: Animal) => number;
type DogComparator = (a: Dog, b: Dog) => number;

const dogComp: DogComparator = (a, b) => a.breed.localeCompare(b.breed);

const comp: Comparator = dogComp;   // โŒ under strictFunctionTypes
comp({ name: 'a' }, { name: 'b' });  // would crash at runtime

dogComp accesses breed, which plain Animals don’t have. Passing plain animals would crash. strictFunctionTypes catches it.

Why the flag is named for functions: It changes the assignability rules for function types, not for classes or objects. Methods are unaffected. The flag targets the specific case where function-typed values are assigned across parameter types.

Why strictFunctionTypes doesn’t cover methods: Methods are called on this more often. The parameter types are less likely to vary. Keeping methods bivariant avoids false positives on common patterns. Function-typed properties are the main risk, and those are covered.


Practical implications

Variance shows up in real code โ€” usually as an error you need to understand.

Callbacks and Array.map:

const dogs: Dog[] = [];
const animals: Animal[] = dogs;

// fine โ€” covariance
animals.map(a => a.name);

// but the callback parameter is typed
animals.map((a: Dog) => a.breed);   // โŒ a is Animal, not Dog

The array is covariant, but the callback receives Animal, not Dog.

Callback variance:

function forEach<T>(items: T[], fn: (item: T) => void): void {
  for (const item of items) fn(item);
}

forEach<Dog>(dogs, (d: Dog) => console.log(d.breed));      // โœ…
forEach<Dog>(dogs, (a: Animal) => console.log(a.name));    // โœ… contravariant
forEach<Dog>(dogs, (b: { name: string }) => {});           // โœ… structurally compatible

fn accepts a Dog (or wider). The contravariance allows any function that can handle a Dog.

Comparator patterns:

type Comparator<T> = (a: T, b: T) => number;

const animalComp: Comparator<Animal> = (a, b) => a.name.localeCompare(b.name);
const dogComp: Comparator<Dog> = (a, b) => a.breed.localeCompare(b.breed);

// A Comparator<Animal> can compare Dogs
const c: Comparator<Dog> = animalComp;   // โœ…
// A Comparator<Dog> cannot compare all Animals
const d: Comparator<Animal> = dogComp;   // โŒ under strictFunctionTypes

The general comparator works for the narrower type; the narrow one doesn’t work for the general type.

React event handlers โ€” covariant event types:

type Handler = (e: MouseEvent) => void;

const specific: (e: Event) => void = e => {};   // wider
const h: Handler = specific;   // โœ… contravariant โ€” accepts Event

Enabling strictFunctionTypes:

{
  "compilerOptions": {
    "strict": true
  }
}

strict: true includes strictFunctionTypes. Keep it on.

Why practical examples matter more than theory: The variance rules are abstract until you see them in a callback error. Once you’ve debugged one, the concept clicks.

Why developers run into variance without knowing it: strictFunctionTypes errors mention contravariance implicitly. The fix is usually to widen the parameter type or convert a method to a function property. Understanding the rules makes the fix obvious.


A full example

A typed event system that exercises variance.

// ============================================
// DOMAIN
// ============================================

interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

interface Cat extends Animal {
  indoor: boolean;
}

// ============================================
// COVARIANT โ€” arrays and returns
// ============================================

const dogs: Dog[] = [
  { name: 'Rex', breed: 'lab' },
  { name: 'Bud', breed: 'pug' }
];

const animals: Animal[] = dogs;   // โœ… covariant
// animals.push({ name: 'Cat' });  // โš ๏ธ  unsound but allowed

// Return types
type Fn = () => Animal;

const returnsDog: () => Dog = () => ({ name: 'd', breed: 'l' });
const fn: Fn = returnsDog;   // โœ… covariant

// ============================================
// CONTRAVARIANT โ€” function parameters
// ============================================

type HandleDog = (d: Dog) => void;
type HandleAnimal = (a: Animal) => void;

const handleAnimal: HandleAnimal = (a) => console.log(a.name);
const handleDog: HandleDog = (d) => console.log(d.breed);

// A handler that takes Animal can handle Dog
const h1: HandleDog = handleAnimal;   // โœ… contravariant

// A handler that takes Dog can't handle all Animals
// const h2: HandleAnimal = handleDog;   // โŒ under strictFunctionTypes

// ============================================
// INVARIANT โ€” parameter and return
// ============================================

type Identity<T> = (x: T) => T;

const animalId: Identity<Animal> = (a) => a;
const dogId: Identity<Dog> = (d) => d;

// const i1: Identity<Dog> = animalId;   // โŒ
// const i2: Identity<Animal> = dogId;   // โŒ
// Neither direction โ€” invariant

// ============================================
// BIVARIANT โ€” methods
// ============================================

interface AnimalStore {
  add(item: Animal): void;   // method โ€” bivariant
}

interface AnimalStoreFn {
  add: (item: Animal) => void;   // property โ€” contravariant
}

const store1: AnimalStore = {
  add: (d: Dog) => console.log(d.breed)   // โœ… bivariant method
};

// const store2: AnimalStoreFn = {
//   add: (d: Dog) => console.log(d.breed)   // โŒ under strictFunctionTypes
// };

// ============================================
// GENERIC UTILITIES โ€” variance of T
// ============================================

// Covariant: T only in return
type Producer<T> = () => T;

// Contravariant: T only in parameter
type Consumer<T> = (value: T) => void;

// Invariant: T in both
type Transformer<T> = (value: T) => T;

// ============================================
// USAGE
// ============================================

function processAnimals(animals: Animal[]): void {
  for (const a of animals) console.log(a.name);
}

processAnimals(dogs);   // โœ… covariance

function processWithHandler(
  items: Dog[],
  handler: (d: Dog) => void
): void {
  for (const item of items) handler(item);
}

processWithHandler(dogs, handleAnimal);   // โœ… contravariance
processWithHandler(dogs, handleDog);      // โœ… exact match

const producer: Producer<Dog> = () => ({ name: 'd', breed: 'l' });
const animalProducer: Producer<Animal> = producer;   // โœ… covariant

const consumer: Consumer<Animal> = (a) => console.log(a.name);
const dogConsumer: Consumer<Dog> = consumer;   // โœ… contravariant

// const animalConsumer: Consumer<Animal> = dogConsumer;  // โŒ

console.log(animals, fn, h1, producer, consumer);

What this shows:

  • Covariance โ€” Dog[] โ†’ Animal[], () => Dog โ†’ () => Animal
  • Contravariance โ€” (a: Animal) => void โ†’ (d: Dog) => void
  • Invariance โ€” (x: T) => T โ€” neither direction
  • Bivariance โ€” method add(item: Animal) accepts add(d: Dog)
  • Producer<T> โ€” covariant (T only in return)
  • Consumer<T> โ€” contravariant (T only in parameter)
  • Transformer<T> โ€” invariant (T in both)

Every variance is exercised. The compiler allows the safe directions and rejects the unsafe ones.

Why this shape: It demonstrates each variance with minimal examples. Once you see the pattern โ€” return covariant, parameter contravariant, both invariant, method bivariant โ€” you can reason about any function or generic type.


Complete Example Session

# ============================================
# PART 1: COVARIANCE โ€” ARRAYS
# ============================================

cat > covariance.ts << 'EOF'
class Animal { name = ''; }
class Dog extends Animal { breed = ''; }

let animals: Animal[] = [];
const dogs: Dog[] = [{ name: 'Rex', breed: 'lab' }];

animals = dogs;   // โœ… covariant

console.log(animals, dogs);
EOF

npx tsc --noEmit covariance.ts
# (no errors)

# ============================================
# PART 2: COVARIANCE โ€” RETURNS
# ============================================

cat > returns.ts << 'EOF'
class Animal { name = ''; }
class Dog extends Animal { breed = ''; }

type Fn = () => Animal;

const returnsDog: () => Dog = () => ({ name: 'd', breed: 'lab' });
const f: Fn = returnsDog;   // โœ…

console.log(f());
EOF

npx tsc --noEmit returns.ts
# (no errors)

# ============================================
# PART 3: CONTRAVARIANCE โ€” PARAMETERS
# ============================================

cat > contravariance.ts << 'EOF'
class Animal { name = ''; }
class Dog extends Animal { breed = ''; }

type HandleAnimal = (a: Animal) => void;
type HandleDog = (d: Dog) => void;

const handleAnimal: HandleAnimal = a => console.log(a.name);
const handleDog: HandleDog = d => console.log(d.breed);

const h1: HandleDog = handleAnimal;   // โœ…
// const h2: HandleAnimal = handleDog;   // โŒ

console.log(h1);
EOF

npx tsc --noEmit contravariance.ts
# (no errors)

# ============================================
# PART 4: INVARIANCE
# ============================================

cat > invariance.ts << 'EOF'
class Animal { name = ''; }
class Dog extends Animal { breed = ''; }

type Identity<T> = (x: T) => T;

const animalId: Identity<Animal> = a => a;
const dogId: Identity<Dog> = d => d;

// const i1: Identity<Dog> = animalId;   // โŒ
// const i2: Identity<Animal> = dogId;   // โŒ

console.log(animalId, dogId);
EOF

npx tsc --noEmit invariance.ts
# (no errors)

# ============================================
# PART 5: BIVARIANCE โ€” METHODS
# ============================================

cat > bivariance.ts << 'EOF'
class Animal { name = ''; }
class Dog extends Animal { breed = ''; }

interface Store {
  add(item: Animal): void;
}

const store: Store = {
  add: (d: Dog) => console.log(d.breed)
};

console.log(store);
EOF

npx tsc --noEmit bivariance.ts
# (no errors)

# ============================================
# PART 6: COMPARATOR EXAMPLE
# ============================================

cat > comparator.ts << 'EOF'
class Animal { name = ''; }
class Dog extends Animal { breed = ''; }

type Comparator<T> = (a: T, b: T) => number;

const animalComp: Comparator<Animal> = (a, b) =>
  a.name.localeCompare(b.name);

const dogComp: Comparator<Dog> = (a, b) =>
  a.breed.localeCompare(b.breed);

// Comparator<Animal> can compare Dogs
const c1: Comparator<Dog> = animalComp;   // โœ…

// Comparator<Dog> can't compare all Animals
// const c2: Comparator<Animal> = dogComp;   // โŒ

console.log(c1);
EOF

npx tsc --noEmit comparator.ts
# (no errors)

# ============================================
# PART 7: GENERIC CONTAINERS
# ============================================

cat > containers.ts << 'EOF'
class Animal { name = ''; }
class Dog extends Animal { breed = ''; }

// Covariant โ€” T only in return
type Producer<T> = () => T;
const prod: Producer<Dog> = () => ({ name: 'd', breed: 'l' });
const animalProd: Producer<Animal> = prod;   // โœ…

// Contravariant โ€” T only in parameter
type Consumer<T> = (x: T) => void;
const cons: Consumer<Animal> = a => console.log(a.name);
const dogCons: Consumer<Dog> = cons;   // โœ…

// Invariant โ€” T in both
type Transformer<T> = (x: T) => T;
const animalTrans: Transformer<Animal> = a => a;
const dogTrans: Transformer<Dog> = d => d;
// const t: Transformer<Dog> = animalTrans;   // โŒ
// const t2: Transformer<Animal> = dogTrans;  // โŒ

console.log(animalProd, dogCons, animalTrans, dogTrans);
EOF

npx tsc --noEmit containers.ts
# (no errors)

# ============================================
# PART 8: COMPILE AND RUN
# ============================================

npx tsc covariance.ts returns.ts contravariance.ts invariance.ts bivariance.ts comparator.ts containers.ts
node covariance.js
# [ [ { name: 'Rex', breed: 'lab' } ] [ { name: 'Rex', breed: 'lab' } ] ]

node returns.js
# [ { name: 'd', breed: 'lab' } ]

node contravariance.js
# [ [Function: handleAnimal] ]

node invariance.js
# [ [Function: animalId] [Function: dogId] ]

node bivariance.js
# [ { add: [Function: add] } ]

node comparator.js
# [ [Function: animalComp] ]

node containers.js
# [ [Function] [Function] [Function] [Function] ]

Quick Reference

The Four Variances

VarianceMeaning
CovariantF<A> assignable to F<B>
ContravariantF<B> assignable to F<A>
InvariantNeither
BivariantBoth

Where Each Applies

ContextVariance
Array elementsCovariant
Function returnCovariant
Promise<T>Covariant
ReadonlyArray<T>Covariant
Function parametersContravariant
Method parametersBivariant
(x: T) => TInvariant

strictFunctionTypes

FlagEffect on function parameters
falseBivariant
trueContravariant
On methodsAlways bivariant

Soundness Trade-Offs

TypeSound?Reason
Dog[] โ†’ Animal[]โŒMutation allowed
readonly Dog[] โ†’ readonly Animal[]โœ…No mutation
() => Dog โ†’ () => Animalโœ…Return covariant
(a: Animal) => void โ†’ (d: Dog) => voidโœ…Parameter contravariant
Method (a: Animal) => void โ†’ method (d: Dog) => voidโŒBivariant

Function Type Patterns

TypeVariance of T
() => TCovariant
(x: T) => voidContravariant
(x: T) => TInvariant
(x: () => T) => voidContravariant in outer
(x: (t: T) => void) => voidCovariant in outer

Method vs Function Property

SyntaxVariance
m(x: T): voidBivariant
m: (x: T) => voidContravariant

Practical Rules

NeedUse
Sound covariancereadonly T[]
Contravariant callbackFunction type, not method
Invariant generic(x: T) => T
Safe callbacksstrictFunctionTypes: true
Mutable containersCareful with covariance

Error Messages

ErrorMeaning
not assignableVariance mismatch
strictFunctionTypesParameter too narrow
Property does not existCovariance read error
possibly undefinedContravariant parameter

When Variance Errors Appear

ScenarioError
Callback with narrower paramstrictFunctionTypes
Method assignmentRare
Generic container mutationNone โ€” covariant
Function return mismatchnot assignable
Both parameter and returnInvariant โ€” both directions fail

Comparisons Across Languages

LanguageVariance
TypeScriptCovariant by default, contravariant for functions under strict
JavaInvariant generics, covariant arrays (unsound)
C#Declared variance (in, out)
ScalaDeclared variance, covariant by default
KotlinDeclared variance (in, out)

TypeScript differs โ€” it doesn’t use declared variance. Structural assignability implies variance.

Helper Types for Variance

TypeMeaning
Producer<T>() => T โ€” covariant
Consumer<T>(x: T) => void โ€” contravariant
Transformer<T>(x: T) => T โ€” invariant
Bivariant<T>Method syntax โ€” bivariant

Key Facts

FactDetail
ArraysCovariant, unsound
readonly arraysCovariant, sound
Function returnsCovariant
Function paramsContravariant under strict
MethodsAlways bivariant
strict: trueIncludes strictFunctionTypes
Structural typingDrives variance

Best Practices

โœ… Do This:

// Enable strict mode (includes strictFunctionTypes)
"strict": true                                                  // โœ…

// Use readonly arrays for sound covariance
function process(items: readonly Animal[]): void { }            // โœ…

// Use function-property syntax for contravariant callbacks
type Handler = { on: (e: Animal) => void };                     // โœ…

// Prefer contravariant-safe parameter types
type Comparator<T> = (a: T, b: T) => number;                    // โœ…

// Widen parameter types to accept more
const h: (a: Animal) => void = a => console.log(a.name);        // โœ…

// Use `readonly T[]` for inputs you don't mutate
function render(items: readonly string[]): void { }             // โœ…

// Type callbacks with the actual type they receive
[1, 2].map((n: number) => n * 2);                               // โœ…

// Keep method parameters broad when possible
add(item: Animal): void { }                                     // โœ…

โŒ Don’t Do This:

// Don't disable strictFunctionTypes
"strictFunctionTypes": false  // โš ๏ธ  allows unsafe callbacks     // โš ๏ธ

// Don't narrow callback parameters
const h: (a: Animal) => void = (d: Dog) => console.log(d.breed);
// โŒ under strictFunctionTypes                                    // โŒ

// Don't rely on array covariance for mutation
const animals: Animal[] = dogs;
animals.push({ name: 'generic' });  // โš ๏ธ  unsound                  // โš ๏ธ

// Don't expect mutability to be covariant-sound
function addDefault(box: Box<Animal>): void {
  box.value = { name: 'default' };  // โš ๏ธ  breaks Box<Dog>          // โš ๏ธ
}

// Don't use method syntax when you need contravariance
interface I { m(d: Dog): void }  // โš ๏ธ  bivariant                   // โš ๏ธ

// Don't cast to bypass variance
const c = dogHandler as (a: Animal) => void;  // โš ๏ธ  unsafe         // โš ๏ธ

// Don't fight variance errors with `any`
const h2: (a: Animal) => void = (d: any) => {};  // โš ๏ธ              // โš ๏ธ

// Don't use invariant types where covariant suffices
type Bad<T> = (x: T) => T;  // โš ๏ธ  when () => T would do          // โš ๏ธ

Common Pitfalls

PitfallProblemSolution
Array covariance mutationUnsoundUse readonly T[]
Narrow callback paramError under strictWiden the parameter
Method bivariance surpriseLess strict than expectedUse function syntax
Invariant type confusionBoth directions failRecognize (x: T) => T
Disabling strictFunctionTypesMisses real bugsKeep it on
Assuming all generics covariantNot alwaysCheck T’s positions
Casting past varianceHides unsafetyFix the type
Confusing subtype with assignableVariance is about assignabilitySame thing

Real-World Examples

1. Covariant array

const dogs: Dog[] = [];
const animals: Animal[] = dogs;   // โœ…

2. Sound covariant array

const dogs: readonly Dog[] = [];
const animals: readonly Animal[] = dogs;   // โœ… sound

3. Covariant return

const f: () => Animal = () => new Dog();   // โœ…

4. Contravariant parameter

const h: (d: Dog) => void = (a: Animal) => console.log(a.name);   // โœ…

5. Rejected contravariant direction

// const h: (a: Animal) => void = (d: Dog) => console.log(d.breed);   // โŒ

6. Invariant identity

type Id<T> = (x: T) => T;
// Id<Dog> and Id<Animal> are unrelated

7. Bivariant method

interface I {
  m(a: Animal): void;  // method โ€” bivariant
}

8. Contravariant function property

interface I {
  m: (a: Animal) => void;  // function property โ€” contravariant
}

9. Comparator

type Comparator<T> = (a: T, b: T) => number;
const c: Comparator<Dog> = (a: Animal, b: Animal) => 0;   // โœ…

10. Covariant promise

const pd: Promise<Dog> = Promise.resolve(new Dog());
const pa: Promise<Animal> = pd;   // โœ…

11. Readonly covariance

const dogs: readonly Dog[] = [];
const animals: readonly Animal[] = dogs;   // โœ…

12. Event handler

type ClickHandler = (e: MouseEvent) => void;
const h: ClickHandler = (e: Event) => {};   // โœ… contravariant

13. Producer pattern

type Producer<T> = () => T;
const p: Producer<Dog> = () => new Dog();
const ap: Producer<Animal> = p;   // โœ…

14. Consumer pattern

type Consumer<T> = (x: T) => void;
const c: Consumer<Animal> = a => {};
const dc: Consumer<Dog> = c;   // โœ…

15. Transformer pattern

type Transformer<T> = (x: T) => T;
// invariant โ€” no direction works

16. Callback widening

[1, 2, 3].forEach((n: number | string) => {});   // โœ… wider is fine

17. Comparator rejection

// const c: Comparator<Animal> = (a: Dog, b: Dog) => 0;   // โŒ

18. Sound transform

function map<T, U>(items: readonly T[], fn: (x: T) => U): U[] {
  return items.map(fn);
}

19. strictFunctionTypes error

type H = (a: Animal) => void;
// const h: H = (d: Dog) => {};   // โŒ

20. Explicit variance helper

type Consumer<T> = (x: T) => void;
type Producer<T> = () => T;

Visual: Covariance

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Dog is a subtype of Animal                  โ”‚
โ”‚                                              โ”‚
โ”‚  Dog[] โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ Animal[]                     โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ–บ                                      โ”‚
โ”‚                                              โ”‚
โ”‚  Same direction                              โ”‚
โ”‚  Every Dog is an Animal, so a Dog[] is       โ”‚
โ”‚  an Animal[]                                 โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  () => Dog โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ () => Animal             โ”‚
โ”‚                                              โ”‚
โ”‚  Return type stays same direction            โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Contravariance

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Dog is a subtype of Animal                  โ”‚
โ”‚                                              โ”‚
โ”‚  (x: Animal) => void                         โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ”‚  assignable to                       โ”‚
โ”‚       โ–ผ                                      โ”‚
โ”‚  (x: Dog) => void                            โ”‚
โ”‚                                              โ”‚
โ”‚  Opposite direction                          โ”‚
โ”‚  A function that handles any Animal can      โ”‚
โ”‚  handle a Dog                                 โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  (x: Dog) => void                            โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ”‚  NOT assignable to                   โ”‚
โ”‚       โ–ผ                                      โ”‚
โ”‚  (x: Animal) => void                         โ”‚
โ”‚                                              โ”‚
โ”‚  A function that only handles Dog can't      โ”‚
โ”‚  handle every Animal                          โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Invariance

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  type Id<T> = (x: T) => T                    โ”‚
โ”‚                                              โ”‚
โ”‚  T is in parameter position (contravariant)  โ”‚
โ”‚  T is in return position (covariant)         โ”‚
โ”‚                                              โ”‚
โ”‚  The two directions cancel                   โ”‚
โ”‚                                              โ”‚
โ”‚  Id<Dog> and Id<Animal> are unrelated        โ”‚
โ”‚                                              โ”‚
โ”‚      Id<Dog> โ—„โ”€โ”€โ”€ no โ”€โ”€โ”€โ–บ Id<Animal>         โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Bivariance

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  interface Store {                           โ”‚
โ”‚    add(item: Animal): void;   โ† method       โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  With method syntax:                         โ”‚
โ”‚                                              โ”‚
โ”‚  add(item: Animal) accepts:                  โ”‚
โ”‚    add(item: Animal) โœ…                      โ”‚
โ”‚    add(item: Dog)    โœ…                      โ”‚
โ”‚                                              โ”‚
โ”‚  Both directions allowed                     โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  interface Store {                           โ”‚
โ”‚    add: (item: Animal) => void;  โ† property  โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  With function-property syntax:              โ”‚
โ”‚                                              โ”‚
โ”‚  add(item: Animal) accepts:                  โ”‚
โ”‚    add(item: Animal) โœ…                      โ”‚
โ”‚    add(item: Dog)    โŒ under strict         โ”‚
โ”‚                                              โ”‚
โ”‚  Only contravariant direction                โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Where Each Applies

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Covariant:                                  โ”‚
โ”‚                                              โ”‚
โ”‚  T[]             โ”€โ”€โ–บ array elements          โ”‚
โ”‚  () => T         โ”€โ”€โ–บ return                  โ”‚
โ”‚  Promise<T>      โ”€โ”€โ–บ promise value           โ”‚
โ”‚  readonly T[]    โ”€โ”€โ–บ sound array             โ”‚
โ”‚                                              โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Contravariant:                              โ”‚
โ”‚                                              โ”‚
โ”‚  (x: T) => void  โ”€โ”€โ–บ parameter (strict)      โ”‚
โ”‚  Comparator<T>   โ”€โ”€โ–บ parameter               โ”‚
โ”‚  Handler<T>      โ”€โ”€โ–บ callback                โ”‚
โ”‚                                              โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Invariant:                                  โ”‚
โ”‚                                              โ”‚
โ”‚  (x: T) => T     โ”€โ”€โ–บ both positions          โ”‚
โ”‚  Box<T> mutable  โ”€โ”€โ–บ parameterized by T      โ”‚
โ”‚                                              โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Bivariant:                                  โ”‚
โ”‚                                              โ”‚
โ”‚  m(x: T): void   โ”€โ”€โ–บ method syntax           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Sound vs Unsound

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Dog[] โ”€โ”€โ–บ Animal[]                          โ”‚
โ”‚                                              โ”‚
โ”‚  โš ๏ธ  Unsound                                  โ”‚
โ”‚                                              โ”‚
โ”‚  const dogs: Dog[] = [];                     โ”‚
โ”‚  const animals: Animal[] = dogs;             โ”‚
โ”‚  animals.push({ name: 'generic' });          โ”‚
โ”‚  // dogs now contains a plain Animal         โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  readonly Dog[] โ”€โ”€โ–บ readonly Animal[]        โ”‚
โ”‚                                              โ”‚
โ”‚  โœ… Sound                                     โ”‚
โ”‚                                              โ”‚
โ”‚  const dogs: readonly Dog[] = [];            โ”‚
โ”‚  const animals: readonly Animal[] = dogs;    โ”‚
โ”‚  // animals.push(...)  โŒ prevented          โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Function Type Variance Summary

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  T only in return:                           โ”‚
โ”‚  () => T                                     โ”‚
โ”‚  Covariant                                   โ”‚
โ”‚                                              โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  T only in parameter:                        โ”‚
โ”‚  (x: T) => void                              โ”‚
โ”‚  Contravariant (with strict)                 โ”‚
โ”‚                                              โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  T in both:                                  โ”‚
โ”‚  (x: T) => T                                 โ”‚
โ”‚  Invariant                                   โ”‚
โ”‚                                              โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Nested โ€” depends on position:               โ”‚
โ”‚  (x: () => T) => void                        โ”‚
โ”‚  T is contravariant in outer (through inner) โ”‚
โ”‚                                              โ”‚
โ”‚  (x: (y: T) => void) => void                 โ”‚
โ”‚  T is covariant in outer (double negation)   โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Decision Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Are you assigning a function?               โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ”œโ”€โ”€ Check the return type:             โ”‚
โ”‚       โ”‚   narrower return OK (covariant)     โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ””โ”€โ”€ Check the parameter types:         โ”‚
โ”‚           wider parameters OK (contravariant)โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Are you assigning an array?                 โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ”œโ”€โ”€ Mutable โ”€โ”€โ–บ Covariant (unsound)    โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ””โ”€โ”€ readonly โ”€โ”€โ–บ Covariant (sound)     โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Compile error about functions?              โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ”œโ”€โ”€ Parameter too narrow โ”€โ”€โ–บ widen     โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ””โ”€โ”€ Return mismatch โ”€โ”€โ–บ check subtype  โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Method or function property?                โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ”œโ”€โ”€ Method โ”€โ”€โ–บ bivariant               โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ””โ”€โ”€ Property โ”€โ”€โ–บ contravariant         โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

VarianceDirectionApplies to
CovariantSameArrays, returns, promises
ContravariantOppositeFunction parameters (strict)
InvariantNeither(x: T) => T
BivariantBothMethod parameters

Key takeaways:

  • Variance describes how subtype relationships propagate through type constructors
  • Covariant โ€” Dog[] โ†’ Animal[], () => Dog โ†’ () => Animal โ€” same direction
  • Contravariant โ€” (a: Animal) => void โ†’ (d: Dog) => void โ€” opposite direction
  • Invariant โ€” (x: T) => T โ€” neither direction, both positions used
  • Bivariant โ€” method parameters โ€” both directions, a compatibility compromise
  • strictFunctionTypes (in strict) makes function parameters contravariant
  • Method syntax stays bivariant even under strict; function-property syntax is contravariant
  • Arrays are covariant and unsound โ€” mutation can break the invariant
  • readonly T[] is covariant and sound โ€” no mutation possible
  • Returns and promises are covariant
  • Comparator patterns use contravariance for parameter types
  • Enabling strict catches real callback bugs
  • Widen parameter types to satisfy contravariance
  • Use readonly for inputs you promise not to mutate

Remember: Variance is a consequence of assignability, not a separate rule. Subtype means assignable. Dog[] is assignable to Animal[] because every Dog is an Animal โ€” covariance. (a: Animal) => void is assignable to (d: Dog) => void because a function that handles any Animal can handle a Dog โ€” contravariance. Both positions make it invariant. Method syntax makes it bivariant. Enable strictFunctionTypes, prefer readonly for sound covariance, widen parameters for safe callbacks. Once variance clicks, the compiler’s errors stop being mysterious โ€” they’re just the rules of the type system doing their job.


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!