Variance, briefly
Why a callback that takes Animal is assignable to one that takes Dog — but not the other way around.
A function asks for a Dog. You hand it one that accepts an Animal instead. TypeScript says yes — the more general one is fine. Now flip it: the function asks for an Animal, and you hand it one that only knows Dog. TypeScript refuses. The first move is intuitive; the second one trips people. The whole story has a name: variance. You won't write the word much, but it explains why callback signatures sometimes seem to fail in odd directions.
The easy direction
Most type relationships are covariant. If Dog is a kind of Animal, then a list of Dog is a kind of list of Animal, a function that returns Dog is a kind of function that returns Animal, and so on. Subtype goes in, subtype comes out — direction preserved.
type Animal = { name: string };
type Dog = Animal & { bark(): void };
function getName(a: Animal): string { return a.name; }
const dogs: Dog[] = [{ name: "Rex", bark() {} }];
// Dog[] flows into Animal[] — covariant
const animals: Animal[] = dogs; // ok
animals.forEach(getName);Dog[] is assignable to Animal[] because every Dog is an Animal. Reading the array gives you something that satisfies Animal, which is what the consumer wanted. This is the case your gut already knows.
The flipped direction
Function parameters go the other way. A function that takes Animal is more general — it accepts any animal, including dogs. A function that only takes Dog is more restrictive — it would refuse a non-dog animal. So the more-general function is more useful, and TypeScript treats it as assignable to a slot expecting the less-general one.
type Greet = (a: Animal) => void; // takes any Animal type GreetDog = (d: Dog) => void; // takes only Dog const greetAny: Greet = (a) => console.log(a.name); // A Greet flows into a GreetDog slot — contravariant const handleDog: GreetDog = greetAny; // ok // A GreetDog cannot flow into a Greet slot const greetDogOnly: GreetDog = (d) => d.bark(); const handleAny: Greet = greetDogOnly; // error
The rule looks backward but the logic is sturdy. Pretend the slot expecting GreetDog will only ever pass Dog to its callback. A function that knows how to handle every Animal already knows how to handle a Dog, so it works there. The other direction breaks: a GreetDog only knows how to talk to a Dog, but the slot might pass it some other Animal.
Dog is a subtype of Animal?Where it bites
You almost never type "contravariant" in app code. The place this matters in practice is event handlers and array methods — anywhere a function takes a callback. Variance decides whether your handler shape is acceptable.
type Listener<E> = (event: E) => void;
type ClickEvent = { type: "click"; x: number };
type GenericEvent = { type: string };
function on<E>(name: string, h: Listener<E>) { /* ... */ }
const generic: Listener<GenericEvent> = (e) => console.log(e.type);
// Works: a generic handler can handle a click event
on<ClickEvent>("click", generic);A handler typed for the broad event type slots into a place expecting the narrow one. Try it the other way — give a click-only handler to a slot expecting any event — and TypeScript stops you.
Method shorthand on object types (handle(e: Event): void) is checked bivariantly by default, while arrow-style fields (handle: (e: Event) => void) are contravariant. The flag strictFunctionTypes tightens method shorthand. Most projects turn it on.
Should you care?
For most application code, no. The mental model is a one-liner: more-general callbacks fit into more-specific slots, not the other way. You'll feel it when you assign event handlers, pass comparators to sort, or write generic event-emitter wrappers — and now the weird-looking error message will make sense.
The two times variance shows up explicitly:
- You hit a parameter assignability error in a callback. Look at which side is more general; that one should be on the assignment's right side, not its left.
- You're writing a generic library API and want to enforce a particular variance. TypeScript 4.7 added the
in/out/in outmodifiers (type Box<in T>) to declare variance on a parameter. App code rarely needs them.
Most people learn variance after a confusing assignability error in a callback. That's the right time to learn it — when you have a real example pinned to it. Memorizing the rules without that context fades quickly.
Try it yourself
Dog <: Animal, which is true?const handler: (e: GenericEvent) => void and a slot wanting (e: ClickEvent) => void. Why does the assignment work?