TypeScript Variance Explained: Covariance, Contravariance and Invariance

Why can you assign a Dog[] to an Animal[], yet assigning a (value: Dog) => void to a (value: Animal) => void is an error? Both look like the same "a dog is an animal" relationship, but TypeScript treats them differently. The rule behind the difference is TypeScript variance: how a subtype relationship between two types carries over to types built from them, such as arrays, promises, generics and functions.

This guide explains covariance, contravariance and invariance (plus the bivariance TypeScript still allows in a few places) with examples you can paste into your editor. You will also learn what strictFunctionTypes changes, why arrays are technically unsound, and how the in and out variance annotations added in TypeScript 4.7 work. Every snippet was compiled in strict mode with TypeScript 7.0, and the lines marked as errors do produce errors.

Key Takeaways

  • Covariant means the relationship keeps its direction. Dog is a subtype of Animal, so Producer<Dog> is usable as Producer<Animal>. This fits types that only output values.
  • Contravariant means the direction reverses. is usable as . This fits types that only values.