rgoussu@goussu: ~/library/applicative-architecture
~/library/applicative-architecture cat covariance-and-contravariance.md

Covariance & contravariance

# Which direction subtyping flows through generics and function signatures — and how variance is Liskov substitution formalized, with interface segregation as its enabler.

Conceptsaved 2026-08-08 #variance#covariance#contravariance#type-systems#solid#liskov#interface-segregation

Overview

Variance answers one question: if Cat is a subtype of Animal, what is List<Cat> to List<Animal>? Covariance preserves the direction (List<Cat> usable as List<Animal>), contravariance reverses it (Handler<Animal> usable as Handler<Cat>), and invariance allows neither. The rules are not type-system trivia: they are the Liskov substitution principle made mechanical — a variance annotation is a machine-checked promise about where substitution stays safe — and interface segregation is what makes the annotations possible at all.

Key points

  • The direction is set by data flow. Positions that produce values (return types, read-only collections) can be covariant; positions that consume values (parameters) must be contravariant. A function type is therefore contravariant in its inputs and covariant in its output: (Animal) -> Cat is a perfectly good (Cat) -> Animal.
  • The tie to Liskov substitution: LSP says a subtype may not strengthen preconditions (so overridden methods may widen parameter types — contravariance) and may not weaken postconditions (so they may narrow return types — covariance). Variance rules are LSP's pre/postcondition clauses applied to signatures; a variance error is a substitution that would break a caller.
  • The tie to interface segregation: a type that both produces and consumes T is forced invariant. Splitting it into role interfaces — a producer (covariant) and a consumer (contravariant) — is ISP applied to variance: Iterable<out T> / Comparator<in T> in Kotlin only exist because the fat interface was segregated. Fat interfaces don't just couple clients to methods they don't use; they destroy variance.
  • Use-site vs. declaration-site: Java annotates at use site with wildcards — PECS, Producer extends, Consumer super — pushing the burden onto every signature; Kotlin, C#, and Scala declare out/in (+/-) once on the type, which only works when the interface has a single role (ISP again).
  • The classic unsoundness: Java and C# arrays are covariant, so Animal[] a = new Cat[1]; a[0] = new Dog() compiles and fails at runtime (ArrayStoreException) — the cautionary tale for covariance on a read-write type. TypeScript accepts method-parameter bivariance for pragmatism (strictFunctionTypes tightens standalone function types, methods stay loose).
  • Rule of thumb: read-only → covariant, write-only → contravariant, read-write → invariant (or segregate until the roles separate).

Examples

interface Producer<out T> { fun next(): T }        // covariant: T only flows out
interface Consumer<in T>  { fun accept(item: T) }  // contravariant: T only flows in

val cats: Producer<Cat> = catShelter
val animals: Producer<Animal> = cats               // ok — a Cat producer produces Animals

val animalSink: Consumer<Animal> = zoo
val catSink: Consumer<Cat> = animalSink            // ok — an Animal consumer accepts Cats
// Java, use-site variance — PECS
void copy(List<? extends Animal> src, List<? super Cat> dst)

Practice

  • type-challenges (source) — warm-up reps at the type level; the variance-flavored puzzles sharpen the eye for which positions produce and which consume.
  • Variance workout — LSP & ISP through the type checker (exercise) — the six drills as one project: the array-covariance trap, Rectangle/Square under property tests, the PECS drill, the ISP→variance kata, strictFunctionTypes koans, and LSP as executable contract tests (see Testing strategies).

Related

  • Clean code — home of SOLID; variance is the L and the I doing type-level work.
  • Coupling & cohesion — a variance annotation narrows the contract clients can couple to.
  • Deep dive TypeScript — flags variance and method bivariance as its to-explore; this note is that thread.
  • Deep dive Java — wildcards and PECS are the use-site dialect of these rules.