Function types are contravariant in their parameter types. The types counter-vary. They vary in the opposite direction from each other. So if A extends B
, then ((x: B)=>void) extends ((x: A)=>void)
, not ((x: A)=>void) extends ((x: B)=>void)
. This is a natural consequence of type theory, but you could convince yourself of this necessity by imagining trying to pass narrower/wider types to functions than they expect and seeing what happens. For example, imagine this succeeded:
const fails: (created: Parent) => void =
(created: Child) => { created.name.toUpperCase() };
The function (created: Child) => { created.name.toUpperCase() }
is fine by itself; it accepts a Child
and accesses its name
property, which is a string
, so it has a toUpperCase()
method. But you've assigned it to a variable of type (created: Parent) => void
. And that means you can call fails()
like this:
fails(new Parent()); // okay at compile time, but
// RUNTIME ERROR! created.name is undefined
If fails()
accepts any Parent
, then you can pass it a new Parent()
which isn't a Child
. But now you have a runtime error, because at runtime you're trying to access the toUpperCase()
method of the nonexistent name
property of the Parent
instance you gave it. Oops, we made a mistake somewhere.
And the mistake is precisely that you cannot widen the type that a function expects. TypeScript will helpfully report an error on this kind of incorrect parameter widening if you have the --strictFunctionTypes
compiler option enabled, which is recommended.
You can't widen a parameter type, but you may narrow it. If you had a GrandChild
subclass, you'd be able to do this with no issue:
class GrandChild extends Child {
age: number = 1;
}
const okay: (created: GrandChild) => void =
(created: Child) => { created.name.toUpperCase() };
okay(new GrandChild()); // okay
That's fine because okay()
can only accept GrandChild
instances, each of which is also a Child
instance, which is what the function implementation expects. The implementation does not know that it's being given GrandChild
ren, so it can't access the age
property, but it's safe to do this.
The rule of thumb is that when a type appears in an input position, like a function parameter, the direction of type variance is opposite from how it is in an output position.
Playground link to code