Variance explains many puzzling assignability errors and the few places where TypeScript is deliberately unsound.
If Dog is assignable to Animal, is Container<Dog> assignable to Container<Animal>? It depends on how the type parameter is used — its variance.
- Covariant (output positions): a
() => Dogis usable as() => Animal. Producing something more specific is safe. Readonly properties and return types are covariant. - Contravariant (input positions): a
(a: Animal) => voidis usable as(d: Dog) => void, but not the other way round. A handler that accepts any animal can certainly handle a dog; a dog-only handler can't be trusted with a cat. - Invariant: a type used for both input and output (a mutable
Box<T>withgetandset) is only compatible with exactly the sameT.
With strictFunctionTypes (part of strict), function-typed properties get proper contravariant parameter checks. Two deliberate exceptions remain for practicality:
- Method syntax is bivariant. Parameters of members declared as methods (
handle(x: Dog): void) are checked in both directions; members declared as properties (handle: (x: Dog) => void) are checked strictly. Prefer property syntax for callbacks in your own interfaces when you want the strict check. - Arrays are covariant even though they're mutable, so
const animals: Animal[] = dogs; animals.push(cat)compiles and corruptsdogs. Usereadonlyarrays in parameters to make this honest.
The compiler normally measures variance from a type's structure. You can annotate it explicitly with in (contravariant), out (covariant) or in out (invariant) on type parameters: interface Producer<out T> { make(): T }. These annotations mostly serve as documentation and can speed up checks on large recursive types; they should match what the structure implies, not try to change it.
Example
interface Animal { name: string }
interface Dog extends Animal { bark(): void }
interface Cat extends Animal { meow(): void }
const dog: Dog = { name: "Rex", bark() {} };
// Covariance: producers of Dog are producers of Animal
const makeDog: () => Dog = () => dog;
const makeAnimal: () => Animal = makeDog; // ok
// Contravariance: consumers of Animal can consume Dog
const logAnimal = (a: Animal) => console.log(a.name);
const onDog: (d: Dog) => void = logAnimal; // ok
const dogOnly = (d: Dog) => d.bark();
// @ts-expect-error -- a Dog-only handler can't accept every Animal
const onAnimal: (a: Animal) => void = dogOnly;
// Method syntax is bivariant (unsound, but allowed)
interface Handlers { handle(a: Animal): void }
const h: Handlers = { handle: dogOnly }; // compiles!
// Arrays are covariant (also unsound)
const dogs: Dog[] = [dog];
const animals: Animal[] = dogs;
animals.push({ name: "Tom", meow() {} } as Cat); // dogs now contains a cat
interface Producer<out T> { make(): T }
const p: Producer<Animal> = { make: makeDog };
onDog(dog); makeAnimal(); h.handle(dog); p.make(); void onAnimal;Remember
- Outputs are covariant, inputs contravariant, read-write positions invariant
strictFunctionTypeschecks function-typed properties, not methods- Arrays and method parameters are deliberately unsound (bivariant/covariant)
in/outannotate variance explicitly on type parameters
Watch out for
- Declaring callbacks with method syntax in interfaces and losing the strict parameter check.