|$ curl https://forge-ai.dev/api/markdown?path=docs/typescript/variance
$cat docs/typescript-—-variance-(covariance-&-contravariance).md
updated Today·22 min read·published

TypeScript — Variance (Covariance & Contravariance)

TypeScriptAdvancedTypesAdvanced🎯Free Tools
Introduction

Variance describes how subtyping relationships between type parameters relate to the types that contain them. If Dog is a subtype of Animal, is Box<Dog> a subtype of Box<Animal>? The answer depends on whether Box is covariant, contravariant, or invariant in its parameter.

TypeScript historically treated most generics as invariant (with special-casing for function parameters under strictFunctionTypes). Since TypeScript 4.7 you can declare variance explicitly with in and out on type parameters.

info

Mastery means predicting whether a substitution is sound before the compiler tells you — especially for callbacks and readonly collections.
Covariance, Contravariance, Invariance
KindMeaningExample
CovariantPreserves subtype directionReadonlyArray<Dog> ReadonlyArray<Animal>
ContravariantReverses subtype direction(a: Animal) => void (d: Dog) => void
InvariantNo subtype either wayArray<Dog>Array<Animal>
BivariantBoth directions (unsound)Method params without strictFunctionTypes

Read as “assignable to”. Covariance lets you pass a more specific producer where a more general one is expected. Contravariance lets you pass a more general consumer where a more specific one is expected.

animals.ts
TypeScript
1interface Animal { name: string }
2interface Dog extends Animal { bark(): void }
3interface Cat extends Animal { meow(): void }
4
5declare const dogs: Dog[];
6declare const animals: Animal[];
7
8// Mutable arrays are invariant under strict checks for push/pop safety
9// dogs as Animal[] would allow animals.push(newCat) — unsound
10const bad: Animal[] = dogs; // often allowed historically; prefer ReadonlyArray
11
12const okRead: ReadonlyArray<Animal> = dogs; // covariant — safe (read-only)
13
Function Parameters Are Contravariant

Under strictFunctionTypes, function parameter positions are checked contravariantly. A function that accepts Animal can be used where a function accepting Dog is required — it can handle any Dog. The reverse is unsound.

callbacks.ts
TypeScript
1type Handler<T> = (value: T) => void;
2
3const handleAnimal: Handler<Animal> = (a) => console.log(a.name);
4const handleDog: Handler<Dog> = (d) => d.bark();
5
6// Contravariant in T: Handler<Animal> assignable to Handler<Dog>
7const forDog: Handler<Dog> = handleAnimal; // OK — Animal handler accepts Dogs
8
9// const forAnimal: Handler<Animal> = handleDog; // ERROR — might receive a Cat
10

warning

Method parameters in classes/interfaces remain bivariant for historical React/DOM compatibility. Prefer function-typed properties when you need sound contravariance.
method-vs-prop.ts
TypeScript
1interface Listener {
2 // Method syntax — bivariant parameters (less safe)
3 onEvent(e: Dog): void;
4}
5
6interface ListenerProp {
7 // Property syntax — contravariant under strictFunctionTypes
8 onEvent: (e: Dog) => void;
9}
10
11const animalListener = {
12 onEvent(e: Animal) {
13 console.log(e.name);
14 },
15};
16
17const ok1: Listener = animalListener; // allowed (bivariant)
18const ok2: ListenerProp = animalListener; // also OK (contravariant Animal → Dog)
19
20const dogOnly = {
21 onEvent(e: Dog) {
22 e.bark();
23 },
24};
25// const bad: ListenerProp = dogOnly; // ERROR with property syntax + strictFunctionTypes
26
Return Types Are Covariant

If a function returns a more specific type, it is safe to use wherever a more general return type is expected. Callers only read the result — they never inject a wider value into the return slot.

returns.ts
TypeScript
1type Factory<T> = () => T;
2
3const makeDog: Factory<Dog> = () => ({ name: "Rex", bark() {} });
4const makeAnimal: Factory<Animal> = makeDog; // OK — covariant return
5
6// const makeDog2: Factory<Dog> = () => ({ name: "Anon" }); // ERROR — missing bark
7
in / out Variance Annotations (TS 4.7+)

Mark type parameters as out (covariant / producer), in (contravariant / consumer), or both (invariant). The compiler verifies that usages match the declared variance and enables stronger assignability checks.

variance-annotations.ts
TypeScript
1// Producer — covariant
2interface Producer<out T> {
3 produce(): T;
4}
5
6// Consumer — contravariant
7interface Consumer<in T> {
8 consume(value: T): void;
9}
10
11// Both — invariant
12interface Box<in out T> {
13 get(): T;
14 set(value: T): void;
15}
16
17declare const dogProducer: Producer<Dog>;
18const animalProducer: Producer<Animal> = dogProducer; // OK (out)
19
20declare const animalConsumer: Consumer<Animal>;
21const dogConsumer: Consumer<Dog> = animalConsumer; // OK (in)
22
23declare const dogBox: Box<Dog>;
24// const animalBox: Box<Animal> = dogBox; // ERROR — invariant
25
AnnotationVarianceTypical use
out TCovariantGetters, Promise-like, ReadonlyArray
in TContravariantSetters, event handlers, comparers
in out TInvariantMutable containers
(none)Inferred / checkedCompiler derives from usage

best practice

Annotate public generic APIs in libraries. Variance annotations document intent and catch accidental dual-use of a type parameter.
Readonly Arrays Are Covariant

ReadonlyArray<T> (and readonly T[]) only expose readers. That makes them covariant in T: a readonly list of Dogs is safely a readonly list of Animals.

readonly-cov.ts
TypeScript
1function summarize(items: ReadonlyArray<Animal>): string {
2 return items.map((a) => a.name).join(", ");
3}
4
5const pack: readonly Dog[] = [
6 { name: "Rex", bark() {} },
7 { name: "Fido", bark() {} },
8];
9
10summarize(pack); // OK — covariant
11
12function mutate(items: Animal[]): void {
13 items.push({ name: "Mystery" }); // might not be a Dog
14}
15
16// mutate(pack as Dog[]); // don't — breaks Dog[] invariants
17
🔥

pro tip

Prefer ReadonlyArray / readonly in public function parameters. You get covariance and signal non-mutation.
Promises, Mapped Types & Common Pitfalls

Promise<T> is covariant in T (a Promise of Dog is a Promise of Animal). Mapped mutable containers stay invariant. Mixing variance with conditional types can surprise you when distributivity kicks in.

promise-var.ts
TypeScript
1async function loadDog(): Promise<Dog> {
2 return { name: "Rex", bark() {} };
3}
4
5const p: Promise<Animal> = loadDog(); // OK
6
7type MutableList<T> = { items: T[] }; // invariant in T via items
8type ReadonlyList<T> = { readonly items: readonly T[] }; // covariant-ish via readonly
9
10declare const dogsList: ReadonlyList<Dog>;
11const animalsList: ReadonlyList<Animal> = dogsList; // OK
12

Common mistakes

mistakes.ts
TypeScript
1// 1. Treating Array<T> like ReadonlyArray<T>
2function printNames(animals: Animal[]) {
3 for (const a of animals) console.log(a.name);
4}
5declare const dogs: Dog[];
6printNames(dogs); // allowed, but printNames could push a Cat
7
8// Fix: ReadonlyArray<Animal>
9function printNamesSafe(animals: ReadonlyArray<Animal>) {
10 for (const a of animals) console.log(a.name);
11}
12
13// 2. Forgetting strictFunctionTypes
14// Without it, callback params are bivariant — bugs hide in event systems
15
16// 3. Marking out on a type used in input position
17// interface Bad<out T> { set(v: T): void } // ERROR — T used in contravariant position
18
Designing Library Generics

When publishing types, decide for each type parameter: is it produced, consumed, or both? Split dual-use interfaces into separate Producer/Consumer faces when possible.

split-api.ts
TypeScript
1interface Reader<out T> {
2 read(): T;
3}
4
5interface Writer<in T> {
6 write(value: T): void;
7}
8
9interface ReadWriter<in out T> extends Reader<T>, Writer<T> {}
10
11function pipe<T>(source: Reader<T>, sink: Writer<T>): void {
12 sink.write(source.read());
13}
14
15declare const dogReader: Reader<Dog>;
16declare const animalWriter: Writer<Animal>;
17// pipe(dogReader, animalWriter); // needs T = Dog, Writer<Animal> is Writer<Dog>? Yes (in)
18pipe(dogReader, animalWriter as Writer<Dog>); // or infer carefully
19
📝

note

Variance is about assignability of the generic type itself, not about whether individual values are subtypes.
Variance Checklist
QuestionIf yes…
Only appear in outputs / getters?Use out (covariant)
Only appear in inputs / setters?Use in (contravariant)
Both read and write?Use in out or split types
Public callback API?Prefer property function types + strictFunctionTypes
Collection parameter?Prefer ReadonlyArray
Practice Snippets

Snippet 1 — Producer assignability

Practice snippet 1. Predict the resulting type, then confirm with hover types in your editor.

variance-1.ts
TypeScript
1interface Animal { name: string }
2interface Dog extends Animal { bark(): void }
3interface Producer<out T> { get(): T }
4declare const d: Producer<Dog>;
5const a: Producer<Animal> = d;
6type Check = typeof a;

Snippet 2 — Consumer assignability

Practice snippet 2. Predict the resulting type, then confirm with hover types in your editor.

variance-2.ts
TypeScript
1interface Consumer<in T> { take(v: T): void }
2declare const c: Consumer<Animal>;
3const d: Consumer<Dog> = c;
4type Check = typeof d;

Snippet 3 — Invariant box

Practice snippet 3. Predict the resulting type, then confirm with hover types in your editor.

variance-3.ts
TypeScript
1interface Box<in out T> { get(): T; set(v: T): void }
2declare const b: Box<Dog>;
3type Ok = Box<Dog>;
4// type Bad = Box<Animal>; // not assignable from Box<Dog>
5type Same = Box<Dog> extends Box<Animal> ? true : false;

Snippet 4 — ReadonlyArray

Practice snippet 4. Predict the resulting type, then confirm with hover types in your editor.

variance-4.ts
TypeScript
1type R = ReadonlyArray<Dog> extends ReadonlyArray<Animal> ? true : false;
2type M = Array<Dog> extends Array<Animal> ? true : false;

Snippet 5 — Handler contravariance

Practice snippet 5. Predict the resulting type, then confirm with hover types in your editor.

variance-5.ts
TypeScript
1type H<T> = (x: T) => void;
2type A = H<Animal> extends H<Dog> ? true : false;
3type B = H<Dog> extends H<Animal> ? true : false;

Snippet 6 — Promise covariance

Practice snippet 6. Predict the resulting type, then confirm with hover types in your editor.

variance-6.ts
TypeScript
1type P = Promise<Dog> extends Promise<Animal> ? true : false;

Snippet 7 — Method bivariance trap

Practice snippet 7. Predict the resulting type, then confirm with hover types in your editor.

variance-7.ts
TypeScript
1interface I { m(x: Dog): void }
2type F = (x: Animal) => void;
3const f: F = (x) => console.log(x.name);
4const i: I = { m: f }; // may be allowed — inspect with/without strictFunctionTypes

Snippet 8 — Split reader/writer

Practice snippet 8. Predict the resulting type, then confirm with hover types in your editor.

variance-8.ts
TypeScript
1interface R<out T> { read(): T }
2interface W<in T> { write(v: T): void }
3type Pipe = <T>(r: R<T>, w: W<T>) => void;

Snippet 9 — Mapped readonly

Practice snippet 9. Predict the resulting type, then confirm with hover types in your editor.

variance-9.ts
TypeScript
1type RO<T> = { readonly [K in keyof T]: T[K] };
2type Cov = RO<{ a: Dog }> extends RO<{ a: Animal }> ? true : false;

Snippet 10 — Illegal out usage

Practice snippet 10. Predict the resulting type, then confirm with hover types in your editor.

variance-10.ts
TypeScript
1// Uncomment to see error:
2// interface Bad<out T> { set(v: T): void }
3type Placeholder = never;
📝

note

These snippets are intentionally dense. Prefer understanding one fully over skimming all.
Deep Dive — Assignability Matrix

Work through this matrix whenever you design a generic. For each type parameter, mark every occurrence as an input, output, or both. The union of marks determines variance.

matrix.ts
TypeScript
1type Occ =
2 | { kind: "in"; path: string }
3 | { kind: "out"; path: string };
4
5// Example analysis for a fictional Store<T>
6// get(): T → out
7// set(v: T): void → in
8// items: T[] → in + out (mutable array)
9// => T must be invariant (in out)
10
11interface Store<in out T> {
12 get(): T;
13 set(v: T): void;
14 items: T[];
15}
16
17// ReadonlyStore only has outs → covariant
18interface ReadonlyStore<out T> {
19 get(): T;
20 readonly items: readonly T[];
21}

Function type positions

In , A is a contravariant position and B is covariant. Nested functions flip polarity each time you go into a parameter.

polarity.ts
TypeScript
1// Polarity flips in parameter position
2type F1<T> = (x: T) => void; // T contravariant
3type F2<T> = (f: (x: T) => void) => void; // T covariant (flipped twice... wait once)
4// (x: T) => void → T is in
5// outer param of that function → flips → T is out (covariant)
6
7type Check1 = F1<Animal> extends F1<Dog> ? true : false; // true under strictFunctionTypes
8type Check2 = F2<Dog> extends F2<Animal> ? true : false; // true — covariant
📝

note

Counting polarity flips is the reliable way to reason about higher-order callbacks and generic middleware.
strictFunctionTypes in Practice

Enable strictFunctionTypes (included in strict) so comparing function types uses contravariant parameters. Methods stay bivariant — migrate hot APIs to function properties when soundness matters.

tsconfig-snippet.json
JSON
1{
2 "compilerOptions": {
3 "strict": true,
4 "strictFunctionTypes": true
5 }
6}
event-bus.ts
TypeScript
1type EventMap = {
2 "user:login": { userId: string };
3 "user:add": { sku: string; qty: number };
4};
5
6class Bus {
7 // Property-typed handlers → sound contravariance
8 private handlers: {
9 [K in keyof EventMap]?: Array<(payload: EventMap[K]) => void>;
10 } = {};
11
12 on<K extends keyof EventMap>(
13 event: K,
14 handler: (payload: EventMap[K]) => void,
15 ): void {
16 (this.handlers[event] ??= []).push(handler);
17 }
18
19 emit<K extends keyof EventMap>(event: K, payload: EventMap[K]): void {
20 this.handlers[event]?.forEach((h) => h(payload));
21 }
22}
23
24const bus = new Bus();
25bus.on("user:login", (p) => console.log(p.userId));
26// bus.on("user:login", (p: EventMap["cart:add"]) => {}); // ERROR
API styleParam varianceRecommendation
Method in interfaceBivariantOK for DOM-like; document risk
Function propertyContravariantPrefer for app/domain events
Generic callback defaultDepends on positionAnnotate in/out
Tuples, const, and Variance

Readonly tuples behave like readonly arrays for element variance. Mutable tuple slots that are written keep invariance. Combining as const with readonly parameters preserves literal types and covariant assignability.

tuples.ts
TypeScript
1function firstAnimal(pair: readonly [Animal, Animal]): Animal {
2 return pair[0];
3}
4
5const dogs = [
6 { name: "a", bark() {} },
7 { name: "b", bark() {} },
8] as const satisfies readonly [Dog, Dog];
9
10// Element types are Dog; readonly tuple of Dogs ~ readonly tuple of Animals
11firstAnimal(dogs);
12
13type MutablePair = [Animal, Animal];
14// firstAnimal needs readonly — mutable pair of Dogs is not safely MutablePair
15

best practice

Accept readonly arrays/tuples in libraries unless mutation is part of the contract.
Summary

Variance is the difference between “this generic looks right” and “this generic is sound.” Use out for producers, in for consumers, in out for mutable boxes, and prefer readonly collections plus property-typed callbacks under strictFunctionTypes.

🔥

pro tip

When a variance error appears, do not silence it with assertions — split the type into Reader/Writer faces or narrow the API.
$Blueprint — Engineering Documentation·Section ID: TS-VARIANCE·Revision: 1.0

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.