Reputation: 253
Why this does not work in typescript?
class Parent {
id: string = ''
}
class Child extends Parent{
name: string = ''
}
const fails: (created: Parent) => void = (created: Child) => { return };
const failsToo: ({ created }: { created: Parent }) => void = ({ created }: { created: Child }) => { return };
At least for me the error is very weird:
Type '(created: Child) => void' is not assignable to type '(created: Parent) => void'.
Types of parameters 'created' and 'created' are incompatible.
Property 'name' is missing in type 'Parent' but required in type 'Child'
It seems like it is trying to assign a Parent to a Child, but the in the real code is backwards (Trying to assign a method parameter that is a Child to a Parent. Which it make sense because Child is a super set of Parent)
Am I missing something?
Upvotes: 2
Views: 887
Reputation: 329378
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.
Upvotes: 5
Reputation: 2678
If you look at the type definition of fails, you must be able to pass a parent to in. This means that the implementation of the function you've got could give a runtime error, eg
const fails: (created: Parent) => void = (created: Child) => {
console.log(created.name)
return
}
fails({ id: 'foo' })
This would fail at runtime because field name would not exist on the argument that was passed in.
Upvotes: 2