🚀AdvancedType System Mastery
Variance: Covariance & Contravariance
Understand how types relate to each other through their type parameters, and why function parameters are checked differently from return types.
Overview
Variance describes how a generic type `Box<T>` relates to another `Box<U>` when T and U have a subtype relationship.
**Covariance** means the relationship is preserved: if Dog extends Animal, then `Box<Dog>` is assignable to `Box<Animal>`. Return types are covariant - you can use a more specific return type where a broader one is expected.
**Contravariance** means the relationship is reversed: if Dog extends Animal, then `Callback<Animal>` is assignable to `Callback<Dog>`. Function parameters are contravariant - you need a broader parameter type where a specific one is expected.
**Bivariance** means both directions work. TypeScript historically used bivariance for method parameters (for practical reasons), but strict function types enforce proper contravariance.
Understanding variance explains many confusing TypeScript errors around function types, callbacks, and generic containers.
Key Points
- •Covariant position: return types - assignable in the same direction as the type relationship
- •Contravariant position: function parameter types - assignable in the opposite direction
- •Invariant: a type that is neither covariant nor contravariant (requires exact match)
- •`strictFunctionTypes: true` enforces contravariance for function types (enabled via strict)
- •Method syntax (`method(): T`) is bivariant; function property syntax (`prop: () => T`) is contravariant
- •Arrays are covariant in TypeScript (unsound, but practical); ReadonlyArray is also covariant
- •Variance annotations in TS 4.7+: `in` (contravariant), `out` (covariant) on type parameters
Examples
Covariance in return types
class Animal { breathe(): void {} }
class Dog extends Animal { bark(): void {} }
class Cat extends Animal { meow(): void {} }
// Return types are COVARIANT
// A function returning Dog can be used where Animal is expected
type GetAnimal = () => Animal;
type GetDog = () => Dog;
const getDog: GetDog = () => new Dog();
// OK - Dog is assignable to Animal (covariant return)
const getAnimal: GetAnimal = getDog;
const animal = getAnimal(); // type: Animal
animal.breathe(); // works - breathe is on Animal
// Reverse does NOT work
const getCat: () => Cat = () => new Cat();
// const specificDog: GetDog = getCat; // Error - Cat is not DogContravariance in parameter types
class Animal { name = 'animal'; }
class Dog extends Animal { breed = 'labrador'; }
// Parameter types are CONTRAVARIANT
// A function accepting Animal can be used where Dog-handler is expected
// (because it will always receive at least an Animal)
type HandleDog = (dog: Dog) => void;
type HandleAnimal = (animal: Animal) => void;
const logAnimal: HandleAnimal = (a) => console.log(a.name);
const logDog: HandleDog = (d) => console.log(d.breed);
// OK - HandleAnimal is assignable to HandleDog (contravariant)
// If we need something that handles a Dog, a handler that handles ANY Animal works
const handler: HandleDog = logAnimal; // fine with strictFunctionTypes
// NOT OK - HandleDog is NOT assignable to HandleAnimal (would crash)
// If we need something that handles ANY Animal, a Dog-only handler might fail
// when given a Cat
// const broken: HandleAnimal = logDog; // Error with strictFunctionTypesMethod bivariance vs function property contravariance
// Method syntax: bivariant (both directions allowed - unsound but practical)
interface MethodStyle {
process(x: Dog): void; // method syntax
}
// Function property syntax: contravariant (correct and sound)
interface FuncStyle {
process: (x: Dog) => void; // function property syntax
}
// With strictFunctionTypes, FuncStyle enforces contravariance
// MethodStyle uses bivariance for backwards compatibility
// TypeScript 4.7+ variance annotations
type Provider<out T> = () => T; // explicitly covariant
type Consumer<in T> = (x: T) => void; // explicitly contravariant
type Transformer<in T, out U> = (x: T) => U; // both
// These annotations let TypeScript skip some inference work,
// improving performance on complex generic types
type StringProvider = Provider<string>;
type AnimalProvider = Provider<Animal>;
declare const sp: StringProvider;
// const ap: AnimalProvider = sp; // Error - string is not Animal
class Animal { name = 'animal'; }
class Dog extends Animal { breed = 'lab'; }Common Mistakes
- Assuming arrays are always safe to assign: `Dog[]` is assignable to `Animal[]` in TypeScript, but writing to the array through the `Animal[]` reference could insert a Cat, breaking the Dog-specific code
- Using method syntax when function property syntax is intended: methods are bivariant and bypass contravariance checks, which can hide bugs
- Confusing covariance and contravariance by thinking "subtype means always assignable": with parameters, a broader type is MORE assignable than a narrower one
Note
A mnemonic for contravariance: think about what the caller will pass in. If a handler accepts ANY Animal, you can safely use it where a Dog-handler is needed - every Dog is an Animal so the handler can always handle what it receives. But a Dog-only handler fails when given a Cat.
Exercises
Write TypeScript code to solve each exercise. Click Run to compile and execute it right here, then mark as done.
Loading editor…
Completed: 0 / 3 exercises
Knowledge Check
Question 1 of 4