Typescript is a structurally typed language, ie if two types have a similar structure they are assignable to each other.
// Example 1 - with no Excess property check (Because of implicit narrow type Record<string, string>)
const x: Record<string, string> = { foo: "bar" };
const y: { baz?: number } = x
// Example 2 - with Excess property check
const y1: { baz?: number } = { foo: 'bar' } // Error
// Also to prove Record<string,string> is assignable to { [k: string]: any }
type Assignable = Record<string, string> extends { [k: string]: any } ? true: false // true
Code Plyaground
Here when you do x: Record<string, string> it basically means x: { [k: string]: string } which is wider type than the actual { foo: "bar" }
And for explicitly annotated types, Excess property checks are not done, meaning we can supply additional props to it along with the required prop that is baz.
But since baz is partial and not required any other excess props can be added to it and baz will be inferred as undefined. hence typescript doesn't complain
To prove my point we can check that
Record<string, string> extends { baz? :number } ? true: false will be true
However if we directly try to assign an object literal type of the same Record type, typescript will complain as now the excess property check does happen.
Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments.
If an object literal has any properties that the “target type” doesn’t have, you’ll get an error.
Side Note:
Record<string, string> is not the preferred type for this scenario.
The idiomatic way of doing that is to use an index signature like { [k: string]: string }
Because given x: Record<string, string>, x.foo will apparently be a string at compile-time, but in actuality is likely to be string | undefined.
This is a gap in how --strictNullChecks works