The obvious
In Typescript, when we are trying to assign an object literal to a typed variable, it is reasonable to expect Typescript to check whether there are any extra properties that are not defined in the respective type.
interface Doggo {
name: string;
likesTreatos: boolean;
}
const max: Doggo = {
name: 'Max',
likesTreatos: true,
age: 4
/**
Type '{ name: string; likesTreatos: true; age: number; }'
is not assignable to type 'Doggo'.
Object literal may only specify known properties, and 'age'
does not exist in type 'Doggo'
*/
}
The not so obvious
Alright, let's tweak that piece of code a little bit and see how Typescript reacts.
interface Doggo {
name: string;
likesTreatos: boolean;
}
const dog = {
name: 'Max',
likesTreatos: true,
age: 4
}
const max: Doggo = dog // no errors
Wait a second, the value of dog
that we are trying to assign to our max
is not so different than before, yet Typescript doesn't think that's an issue this time.
The triggers of the excess property check
Typescript does not complain because there is no excess property check triggered. The reason why is because it will only trigger on assignments that involve object literals, and dog
is clearly not one.
On the first example above, our max
variable was actually assigned to an object literal, triggering the check which then threw an error.
We can see a similar behavior when passing function parameters.
interface Doggo {
name: string;
likesTreatos: boolean;
}
function giveSnack(doggo: Doggo) {
console.log(`Snack given to ${doggo.name}`)
}
giveSnack({
name: 'Max',
likesTreatos: true,
age: 4
/**
Argument of type '{ name: string; likesTreatos: true; age: number; }'
is not assignable to parameter of type 'Doggo'.
Object literal may only specify known properties, and 'age'
does not exist in type 'Doggo'
*/
})
const dog = {
name: 'Max',
likesTreatos: true,
age: 4
}
giveSnack(dog) // no errors
But still, why would that not fail..?
It was hard for me to initially accept the fact that Typescript would allow an assignment like that.
But actually, as far as Typescript's structural type system is concerned, this is a totally valid scenario.
Let's take a step back.
interface Doggo {
name: string;
isGoodBoy?: boolean;
}
How big is the domain of values for the Doggo
type?
The answer is, pretty huge!
Basically any object that has a name
property that is a string and any other property besides a non-boolean isGoodBoy
is a valid Doggo.
Based on that, here is a surprisingly valid assignment to a Doggo
typed constant:
interface Doggo {
name: string;
isGoodBoy?: boolean;
}
const max: Doggo = window
window
has a name
property which is a string and does not have a non-boolean isGoodBoy
property (even though it'd be cool).
Going back to our previous example
interface Doggo {
name: string;
likesTreatos: boolean;
}
const dog = {
name: 'max',
likesTreatos: true,
age: 4
}
const max: Doggo = dog // no errors
This starts to make a bit more sense now, the variable dog
is inferred as {name: string, likesTreatos: boolean, age: number}
which is included in the domain of values of the type Doggo
and hence being a valid assignment.
On top of that, the excess property check is not triggered because we are not dealing with object literals.
Final thoughts
One could say that Typescript's structural type checker focuses more on preventing us from writing code that would throw errors during runtime, while the excess property checking tries to identify code which might not be what we intended.
Top comments (1)
Just created an account to like this post.
Best explanation!