TypeScript — Variance (Covariance & Contravariance)
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
| Kind | Meaning | Example |
|---|---|---|
| Covariant | Preserves subtype direction | ReadonlyArray<Dog> ≤ ReadonlyArray<Animal> |
| Contravariant | Reverses subtype direction | (a: Animal) => void ≤ (d: Dog) => void |
| Invariant | No subtype either way | Array<Dog> ≰ Array<Animal> |
| Bivariant | Both 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.
| 1 | interface Animal { name: string } |
| 2 | interface Dog extends Animal { bark(): void } |
| 3 | interface Cat extends Animal { meow(): void } |
| 4 | |
| 5 | declare const dogs: Dog[]; |
| 6 | declare 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 |
| 10 | const bad: Animal[] = dogs; // often allowed historically; prefer ReadonlyArray |
| 11 | |
| 12 | const okRead: ReadonlyArray<Animal> = dogs; // covariant — safe (read-only) |
| 13 |
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.
| 1 | type Handler<T> = (value: T) => void; |
| 2 | |
| 3 | const handleAnimal: Handler<Animal> = (a) => console.log(a.name); |
| 4 | const handleDog: Handler<Dog> = (d) => d.bark(); |
| 5 | |
| 6 | // Contravariant in T: Handler<Animal> assignable to Handler<Dog> |
| 7 | const forDog: Handler<Dog> = handleAnimal; // OK — Animal handler accepts Dogs |
| 8 | |
| 9 | // const forAnimal: Handler<Animal> = handleDog; // ERROR — might receive a Cat |
| 10 |
warning
| 1 | interface Listener { |
| 2 | // Method syntax — bivariant parameters (less safe) |
| 3 | onEvent(e: Dog): void; |
| 4 | } |
| 5 | |
| 6 | interface ListenerProp { |
| 7 | // Property syntax — contravariant under strictFunctionTypes |
| 8 | onEvent: (e: Dog) => void; |
| 9 | } |
| 10 | |
| 11 | const animalListener = { |
| 12 | onEvent(e: Animal) { |
| 13 | console.log(e.name); |
| 14 | }, |
| 15 | }; |
| 16 | |
| 17 | const ok1: Listener = animalListener; // allowed (bivariant) |
| 18 | const ok2: ListenerProp = animalListener; // also OK (contravariant Animal → Dog) |
| 19 | |
| 20 | const dogOnly = { |
| 21 | onEvent(e: Dog) { |
| 22 | e.bark(); |
| 23 | }, |
| 24 | }; |
| 25 | // const bad: ListenerProp = dogOnly; // ERROR with property syntax + strictFunctionTypes |
| 26 |
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.
| 1 | type Factory<T> = () => T; |
| 2 | |
| 3 | const makeDog: Factory<Dog> = () => ({ name: "Rex", bark() {} }); |
| 4 | const makeAnimal: Factory<Animal> = makeDog; // OK — covariant return |
| 5 | |
| 6 | // const makeDog2: Factory<Dog> = () => ({ name: "Anon" }); // ERROR — missing bark |
| 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.
| 1 | // Producer — covariant |
| 2 | interface Producer<out T> { |
| 3 | produce(): T; |
| 4 | } |
| 5 | |
| 6 | // Consumer — contravariant |
| 7 | interface Consumer<in T> { |
| 8 | consume(value: T): void; |
| 9 | } |
| 10 | |
| 11 | // Both — invariant |
| 12 | interface Box<in out T> { |
| 13 | get(): T; |
| 14 | set(value: T): void; |
| 15 | } |
| 16 | |
| 17 | declare const dogProducer: Producer<Dog>; |
| 18 | const animalProducer: Producer<Animal> = dogProducer; // OK (out) |
| 19 | |
| 20 | declare const animalConsumer: Consumer<Animal>; |
| 21 | const dogConsumer: Consumer<Dog> = animalConsumer; // OK (in) |
| 22 | |
| 23 | declare const dogBox: Box<Dog>; |
| 24 | // const animalBox: Box<Animal> = dogBox; // ERROR — invariant |
| 25 |
| Annotation | Variance | Typical use |
|---|---|---|
| out T | Covariant | Getters, Promise-like, ReadonlyArray |
| in T | Contravariant | Setters, event handlers, comparers |
| in out T | Invariant | Mutable containers |
| (none) | Inferred / checked | Compiler derives from usage |
best practice
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.
| 1 | function summarize(items: ReadonlyArray<Animal>): string { |
| 2 | return items.map((a) => a.name).join(", "); |
| 3 | } |
| 4 | |
| 5 | const pack: readonly Dog[] = [ |
| 6 | { name: "Rex", bark() {} }, |
| 7 | { name: "Fido", bark() {} }, |
| 8 | ]; |
| 9 | |
| 10 | summarize(pack); // OK — covariant |
| 11 | |
| 12 | function 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
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.
| 1 | async function loadDog(): Promise<Dog> { |
| 2 | return { name: "Rex", bark() {} }; |
| 3 | } |
| 4 | |
| 5 | const p: Promise<Animal> = loadDog(); // OK |
| 6 | |
| 7 | type MutableList<T> = { items: T[] }; // invariant in T via items |
| 8 | type ReadonlyList<T> = { readonly items: readonly T[] }; // covariant-ish via readonly |
| 9 | |
| 10 | declare const dogsList: ReadonlyList<Dog>; |
| 11 | const animalsList: ReadonlyList<Animal> = dogsList; // OK |
| 12 |
Common mistakes
| 1 | // 1. Treating Array<T> like ReadonlyArray<T> |
| 2 | function printNames(animals: Animal[]) { |
| 3 | for (const a of animals) console.log(a.name); |
| 4 | } |
| 5 | declare const dogs: Dog[]; |
| 6 | printNames(dogs); // allowed, but printNames could push a Cat |
| 7 | |
| 8 | // Fix: ReadonlyArray<Animal> |
| 9 | function 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 |
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.
| 1 | interface Reader<out T> { |
| 2 | read(): T; |
| 3 | } |
| 4 | |
| 5 | interface Writer<in T> { |
| 6 | write(value: T): void; |
| 7 | } |
| 8 | |
| 9 | interface ReadWriter<in out T> extends Reader<T>, Writer<T> {} |
| 10 | |
| 11 | function pipe<T>(source: Reader<T>, sink: Writer<T>): void { |
| 12 | sink.write(source.read()); |
| 13 | } |
| 14 | |
| 15 | declare const dogReader: Reader<Dog>; |
| 16 | declare const animalWriter: Writer<Animal>; |
| 17 | // pipe(dogReader, animalWriter); // needs T = Dog, Writer<Animal> is Writer<Dog>? Yes (in) |
| 18 | pipe(dogReader, animalWriter as Writer<Dog>); // or infer carefully |
| 19 |
note
| Question | If 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 |
Snippet 1 — Producer assignability
Practice snippet 1. Predict the resulting type, then confirm with hover types in your editor.
| 1 | interface Animal { name: string } |
| 2 | interface Dog extends Animal { bark(): void } |
| 3 | interface Producer<out T> { get(): T } |
| 4 | declare const d: Producer<Dog>; |
| 5 | const a: Producer<Animal> = d; |
| 6 | type Check = typeof a; |
Snippet 2 — Consumer assignability
Practice snippet 2. Predict the resulting type, then confirm with hover types in your editor.
| 1 | interface Consumer<in T> { take(v: T): void } |
| 2 | declare const c: Consumer<Animal>; |
| 3 | const d: Consumer<Dog> = c; |
| 4 | type Check = typeof d; |
Snippet 3 — Invariant box
Practice snippet 3. Predict the resulting type, then confirm with hover types in your editor.
| 1 | interface Box<in out T> { get(): T; set(v: T): void } |
| 2 | declare const b: Box<Dog>; |
| 3 | type Ok = Box<Dog>; |
| 4 | // type Bad = Box<Animal>; // not assignable from Box<Dog> |
| 5 | type 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.
| 1 | type R = ReadonlyArray<Dog> extends ReadonlyArray<Animal> ? true : false; |
| 2 | type 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.
| 1 | type H<T> = (x: T) => void; |
| 2 | type A = H<Animal> extends H<Dog> ? true : false; |
| 3 | type 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.
| 1 | type 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.
| 1 | interface I { m(x: Dog): void } |
| 2 | type F = (x: Animal) => void; |
| 3 | const f: F = (x) => console.log(x.name); |
| 4 | const 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.
| 1 | interface R<out T> { read(): T } |
| 2 | interface W<in T> { write(v: T): void } |
| 3 | type 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.
| 1 | type RO<T> = { readonly [K in keyof T]: T[K] }; |
| 2 | type 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.
| 1 | // Uncomment to see error: |
| 2 | // interface Bad<out T> { set(v: T): void } |
| 3 | type Placeholder = never; |
note
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.
| 1 | type 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 | |
| 11 | interface Store<in out T> { |
| 12 | get(): T; |
| 13 | set(v: T): void; |
| 14 | items: T[]; |
| 15 | } |
| 16 | |
| 17 | // ReadonlyStore only has outs → covariant |
| 18 | interface 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.
| 1 | // Polarity flips in parameter position |
| 2 | type F1<T> = (x: T) => void; // T contravariant |
| 3 | type 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 | |
| 7 | type Check1 = F1<Animal> extends F1<Dog> ? true : false; // true under strictFunctionTypes |
| 8 | type Check2 = F2<Dog> extends F2<Animal> ? true : false; // true — covariant |
note
Enable strictFunctionTypes (included in strict) so comparing function types uses contravariant parameters. Methods stay bivariant — migrate hot APIs to function properties when soundness matters.
| 1 | { |
| 2 | "compilerOptions": { |
| 3 | "strict": true, |
| 4 | "strictFunctionTypes": true |
| 5 | } |
| 6 | } |
| 1 | type EventMap = { |
| 2 | "user:login": { userId: string }; |
| 3 | "user:add": { sku: string; qty: number }; |
| 4 | }; |
| 5 | |
| 6 | class 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 | |
| 24 | const bus = new Bus(); |
| 25 | bus.on("user:login", (p) => console.log(p.userId)); |
| 26 | // bus.on("user:login", (p: EventMap["cart:add"]) => {}); // ERROR |
| API style | Param variance | Recommendation |
|---|---|---|
| Method in interface | Bivariant | OK for DOM-like; document risk |
| Function property | Contravariant | Prefer for app/domain events |
| Generic callback default | Depends on position | Annotate in/out |
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.
| 1 | function firstAnimal(pair: readonly [Animal, Animal]): Animal { |
| 2 | return pair[0]; |
| 3 | } |
| 4 | |
| 5 | const 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 |
| 11 | firstAnimal(dogs); |
| 12 | |
| 13 | type MutablePair = [Animal, Animal]; |
| 14 | // firstAnimal needs readonly — mutable pair of Dogs is not safely MutablePair |
| 15 |
best practice
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
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.