Lesson 65 of 80
A function is assignable where a wider one is expected only if its return type is at least as specific and its parameter types are at least as general — never the other way around.
16 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
When is a function of one type safe to use in place of another? TypeScript answers this with variance: the direction a type is allowed to change while a relationship still holds.
Return types are covariant — a function returning something more specific than required is fine, because the caller only ever uses what it asked for. Parameter types are contravariant — a function that accepts something broader than required is fine, because it's still guaranteed to handle whatever narrower thing actually gets passed. Both rules point the same direction once you say them plainly: a substitute function must promise to give back at least as much as expected, and must be willing to accept at least as much as it might receive.
interface Animal { name: string; }
interface Dog extends Animal { breed: string; }
type Handler = (event: Animal) => void;
type AnimalFactory = () => Animal;
// ✅ covariant return — Dog is more specific than Animal, so a function
// that returns a Dog can stand in anywhere an Animal-returning function is expected
const makeDog: () => Dog = () => ({ name: "Rex", breed: "Lab" });
const factory: AnimalFactory = makeDog; // ✅ narrower return is assignable to wider
// ✅ contravariant parameter — this handler accepts ANY Animal, so it's safe
// to register somewhere that will only ever hand it Dogs specifically
const logAnyAnimal: (event: Animal) => void = (event) => console.log(event.name);
const dogHandler: (event: Dog) => void = logAnyAnimal; // ✅ wider param is assignable to narrower
// ❌ the reverse fails — a handler that only knows about Dog.breed
// can't safely stand in for "accepts any Animal"
const logDogOnly: (event: Dog) => void = (event) => console.log(event.breed);
const wrongHandler: Handler = logDogOnly; // ❌ error under strictFunctionTypes
logAnyAnimal accepts Animal, which is broader than Dog — assigning it to a slot that expects (event: Dog) => void is safe, because anything that can handle any Animal can certainly handle the Dogs it'll actually receive. The reverse is unsound: logDogOnly reads event.breed, and a plain Animal doesn't have one — that's exactly the case strictFunctionTypes exists to catch.
Array<Dog>.forEach accepts a callback typed (dog: Dog) => void — but a callback typed (animal: Animal) => void is also accepted, because it can handle any Dog it's handed. That's contravariance, used constantly without anyone naming it.handle(event: Animal): void to handle: (event: Animal) => void can change whether a particular assignment type-checks — it's a real, easy-to-miss gotcha worth knowing exists."If a function expects a callback of type (x: Dog) => void, can I pass one typed (x: Animal) => void instead?" is a compact way to test whether a candidate actually understands variance rather than just pattern-matching "TypeScript is picky about function types" — the correct answer, and the reasoning for it, is the whole lesson in one sentence.