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:
Ais a subtype ofBFis a type constructor (likeArray,Promise,() => _)
What is the relationship between F<A> and F<B>?
| Variance | Relationship |
|---|---|
| Covariant | F<A> is a subtype of F<B> |
| Contravariant | F<B> is a subtype of F<A> |
| Invariant | Neither โ F<A> and F<B> are unrelated |
| Bivariant | Both โ 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:
| Context | Variance of the type parameter |
|---|---|
| Array elements | Covariant |
| Function return | Covariant |
| Function parameters | Contravariant |
| Method parameters | Bivariant |
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
Dogis anAnimal. A value that producesDogs producesAnimals. 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
Animalneeds less than one requiringDog. 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
HandleDogassignable toHandleAnimalmeans you could pass aCatat 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:Tis used as both input and output. Input is contravariant; output is covariant. Two opposite directions with the same type parameter cancel. NeitherFn<Dog>norFn<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.mapcallbacks- 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
strictFunctionTypesdoesn’t cover methods: Methods are called onthismore 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:
strictFunctionTypeserrors 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)acceptsadd(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
| Variance | Meaning |
|---|---|
| Covariant | F<A> assignable to F<B> |
| Contravariant | F<B> assignable to F<A> |
| Invariant | Neither |
| Bivariant | Both |
Where Each Applies
| Context | Variance |
|---|---|
| Array elements | Covariant |
| Function return | Covariant |
Promise<T> | Covariant |
ReadonlyArray<T> | Covariant |
| Function parameters | Contravariant |
| Method parameters | Bivariant |
(x: T) => T | Invariant |
strictFunctionTypes
| Flag | Effect on function parameters |
|---|---|
false | Bivariant |
true | Contravariant |
| On methods | Always bivariant |
Soundness Trade-Offs
| Type | Sound? | 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
| Type | Variance of T |
|---|---|
() => T | Covariant |
(x: T) => void | Contravariant |
(x: T) => T | Invariant |
(x: () => T) => void | Contravariant in outer |
(x: (t: T) => void) => void | Covariant in outer |
Method vs Function Property
| Syntax | Variance |
|---|---|
m(x: T): void | Bivariant |
m: (x: T) => void | Contravariant |
Practical Rules
| Need | Use |
|---|---|
| Sound covariance | readonly T[] |
| Contravariant callback | Function type, not method |
| Invariant generic | (x: T) => T |
| Safe callbacks | strictFunctionTypes: true |
| Mutable containers | Careful with covariance |
Error Messages
| Error | Meaning |
|---|---|
not assignable | Variance mismatch |
strictFunctionTypes | Parameter too narrow |
Property does not exist | Covariance read error |
possibly undefined | Contravariant parameter |
When Variance Errors Appear
| Scenario | Error |
|---|---|
| Callback with narrower param | strictFunctionTypes |
| Method assignment | Rare |
| Generic container mutation | None โ covariant |
| Function return mismatch | not assignable |
| Both parameter and return | Invariant โ both directions fail |
Comparisons Across Languages
| Language | Variance |
|---|---|
| TypeScript | Covariant by default, contravariant for functions under strict |
| Java | Invariant generics, covariant arrays (unsound) |
| C# | Declared variance (in, out) |
| Scala | Declared variance, covariant by default |
| Kotlin | Declared variance (in, out) |
TypeScript differs โ it doesn’t use declared variance. Structural assignability implies variance.
Helper Types for Variance
| Type | Meaning |
|---|---|
Producer<T> | () => T โ covariant |
Consumer<T> | (x: T) => void โ contravariant |
Transformer<T> | (x: T) => T โ invariant |
Bivariant<T> | Method syntax โ bivariant |
Key Facts
| Fact | Detail |
|---|---|
| Arrays | Covariant, unsound |
readonly arrays | Covariant, sound |
| Function returns | Covariant |
| Function params | Contravariant under strict |
| Methods | Always bivariant |
strict: true | Includes strictFunctionTypes |
| Structural typing | Drives 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
| Pitfall | Problem | Solution |
|---|---|---|
| Array covariance mutation | Unsound | Use readonly T[] |
| Narrow callback param | Error under strict | Widen the parameter |
| Method bivariance surprise | Less strict than expected | Use function syntax |
| Invariant type confusion | Both directions fail | Recognize (x: T) => T |
Disabling strictFunctionTypes | Misses real bugs | Keep it on |
| Assuming all generics covariant | Not always | Check T’s positions |
| Casting past variance | Hides unsafety | Fix the type |
| Confusing subtype with assignable | Variance is about assignability | Same 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
| Variance | Direction | Applies to |
|---|---|---|
| Covariant | Same | Arrays, returns, promises |
| Contravariant | Opposite | Function parameters (strict) |
| Invariant | Neither | (x: T) => T |
| Bivariant | Both | Method 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(instrict) 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
strictcatches real callback bugs - Widen parameter types to satisfy contravariance
- Use
readonlyfor 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!