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
inorout. 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
| Kind | Direction | Typical Position |
|---|---|---|
| Covariant | A <: B → F<A> <: F<B> | Return type, readonly property |
| Contravariant | A <: B → F<B> <: F<A> | Function parameter |
| Invariant | Neither direction | Mutable property, read-write |
| Bivariant | Both directions | Method parameters (TypeScript legacy) |
Position Rules
| Position | Variance | Sound? |
|---|---|---|
() => T | Covariant in T | ✅ |
(x: T) => void | Contravariant in T | ✅ under strictFunctionTypes |
readonly T | Covariant 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
| Annotation | Meaning | Checked |
|---|---|---|
out T | Covariant | T only in output positions |
in T | Contravariant | T only in input positions |
in out T | Invariant | T in both positions |
strictFunctionTypes
| Setting | Parameter Behavior |
|---|---|
false | Bivariant (both directions allowed) |
true | Contravariant (wider only) |
| Method syntax | Bivariant regardless |
| Function property syntax | Follows the flag |
Assignability Cheat Sheet
| From | To | Allowed? |
|---|---|---|
() => 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
| Pitfall | Problem | Solution |
|---|---|---|
| Narrower callback parameter | Rejected under strictFunctionTypes | Widen the parameter |
| Array covariance surprise | Non-Dog value in a Dog[] | Use readonly arrays |
| Method bivariance | Unsound assignments compile | Use property syntax |
Wrong in/out annotation | Error at declaration | Match annotation to use |
Box<T> assumed covariant | Invariant, rejected | Split into producer/consumer |
strictFunctionTypes off | Unsound code accepted | Enable strict |
Mixing in out | No substitution allowed | Use explicit producer/consumer types |
| Expecting soundness | TypeScript is intentionally unsound | Know 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
| Concept | Rule | Example |
|---|---|---|
| Covariance | A <: B → F<A> <: F<B> | Return type, readonly |
| Contravariance | A <: B → F<B> <: F<A> | Function parameter |
| Invariance | No substitution | Mutable property |
| Bivariance | Both directions | Method parameters |
out T | Declares covariant | Producer |
in T | Declares contravariant | Consumer |
in out T | Declares invariant | Read-write |
strictFunctionTypes | Makes parameters contravariant | Part of strict |
| Mutable array | Covariant, unsound | Dog[] → Animal[] |
readonly array | Covariant, sound | Safe |
Key takeaways:
- Variance describes substitutability of parameterized types — whether
F<A>can replaceF<B>whenAandBare 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
strictFunctionTypesis on - Method parameters stay bivariant in TypeScript, a deliberate unsoundness for ergonomics
- Arrays are covariant and unsound —
Dog[]is assignable toAnimal[], and a push can break the invariant;readonlyarrays are sound inandoutannotations 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!