Q29 of 38 · TypeScript

What is type variance in TypeScript, and why does it matter for function types?

TypeScriptSeniortypescriptvariancecovariancecontravariancestrictFunctionTypesadvanced-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.on event 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

// WHAT INTERVIEWERS LOOK FOR

The definition of covariance and contravariance with concrete examples. Why `strictFunctionTypes` enforces contravariance for parameters. TypeScript 4.7 variance annotations as a bonus. This is genuinely advanced — a correct answer demonstrates deep type system understanding.

// COMMON PITFALL

Thinking function parameters are covariant — before `strictFunctionTypes`, TypeScript treated method parameters bivariantly (allowing both directions) for practicality. `strictFunctionTypes` corrects this for function types but not method syntax.