Q29 of 38 · TypeScript
What is type variance in TypeScript, and why does it matter for function types?
Short answer
Short answer: Variance describes how subtype relationships propagate through generic types. Covariant types (`T` in output positions) preserve subtype direction. Contravariant types (function parameters with `strictFunctionTypes`) invert it. Understanding this prevents subtle type errors when assigning callback-heavy APIs in test utilities.
Detail
Variance is about how the subtype relationship between Dog and Animal flows through a generic container Container<T>.
Covariance (output position): Container<Dog> is assignable to Container<Animal> if Dog extends Animal. Return types are covariant — a function that returns Dog is usable where one returning Animal is expected.
Contravariance (input position): Function parameters are contravariant — a function accepting Animal is usable where one accepting Dog is expected (not the other way around). This is enforced by strictFunctionTypes.
Invariance: A type that is neither covariant nor contravariant — both input and output. Arrays in TypeScript are bivariant by default (for historical compatibility), but function parameters with strictFunctionTypes are contravariant.
Why it matters for testing:
- Callback types in test helpers that accept "event handler of type X" have contravariant parameter types.
- Misunderstanding variance causes puzzling type errors when assigning mock functions.
- Playwright's
page.onevent callbacks must accept the event's specific type, not a broader one.
TypeScript 4.7+: Explicit variance annotations: in (contravariant), out (covariant), in out (invariant) in generic type parameters.
// EXAMPLE
class Animal { name: string = ""; }
class Dog extends Animal { breed: string = ""; }
// Covariant return type — OK
type GetAnimal = () => Animal;
type GetDog = () => Dog;
const getAnimal: GetAnimal = (() => new Dog()); // Dog is assignable to Animal position
// Contravariant parameters — with strictFunctionTypes
type HandleAnimal = (a: Animal) => void;
type HandleDog = (d: Dog) => void;
const handleAnimal: HandleAnimal = (a) => console.log(a.name);
// const handleDog: HandleDog = handleAnimal; // OK: accepts Animal ⊇ Dog, safe
// const handleAnimal2: HandleAnimal = (d: Dog) => d.breed; // Error: Dog param not assignable to Animal
// TypeScript 4.7 explicit variance
type Provider<out T> = () => T; // covariant output only
type Consumer<in T> = (v: T) => void; // contravariant input only
type Transformer<in out T> = (v: T) => T; // invariant