Typescript is a language with structural typing, meaning if two types have similar structures, they can be assigned to each other.
// Example 1 - No Excess property check (due to 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
// Demonstrating Record<string,string> is assignable to { [k: string]: any }
type Assignable = Record<string, string> extends { [k: string]: any } ? true: false // true
Play around with the code
In the statement x: Record<string, string>
, it essentially means x: { [k: string]: string }
which is a broader type than { foo: "bar" }
.
When types are explicitly annotated, there are no excess property checks conducted. This allows additional properties along with the required baz
prop in the assignment.
The ability to add extra props works because baz
is optional and accepts undefined values, hence Typescript doesn't flag an error.
We can verify that
Record<string, string> extends { baz? :number } ? true: false
will return
true
However, directly assigning an object literal of the same Record type triggers a complaint from Typescript due to the excess property check.
Object literals undergo excess property checking when assigned to variables or passed as arguments. Any properties in the literal not present in the "target type" result in an error.
Note:
Record<string, string>
may not be the best choice in this case.
The recommended approach is using an index signature like { [k: string]: string }
With x: Record<string, string>
, x.foo
might seem to be a string at compile-time but could actually be string | undefined
.
This discrepancy arises from how --strictNullChecks
functions