The reason why this works is the assignability of an intersection type to its base types.
As an intersection type, Examples
is assignable to ExampleA
. ExampleA
is assignable to { [key: string]: string }
. Therefore, Examples
must be assignable to the function parameter type
This can be shown in this code:
const bar: Examples = { a: 'foo', b: 1, c: false };
const bar2: ExampleA = bar;
const bar3: { [key: string]: string } = bar2;
foo(bar3); //This works
foo(bar2); //Since the assignment bar3 = bar2 works, this must work, too
foo(bar); //Since the assignment bar2 = bar works, this must work, too
Playground version
UPDATE
The behavior is consequential when you want to uphold the principle "when A is assignable to B and B is assignable to C, then A must be assignable to C". The type system has no other choice than to allow these kind of assignents. However, there actually is another issue in passing the value as a parameter to foo
.
You can assign a value to a variable of a type that is sharing only a part of the members of the assigned value. So this assignment works fine:
let item: { a: string, b: number } = { a: "Hello World!", b: 1 };
let partiallyMatchingItem: { a: string } = item;
It is absolutely no problem that partiallyMatchingItem
has more properties than actually declared in the type. The guarantee is a minimum guarantee.
The assignment to a mapped type however does not work, because item
's additional member of type number
:
let item = { a: "Hello World!", b: 1 };
let mappedTypeItem: { [key: string]: string } = item; //Error
So the guarante this time is not a minimum guarantee, it is an absolute guarantee. And that is quite ridiculous, when you consider how easily you can get around it (intentionally or accidentally):
let item = { a: "Hello World!", b: 1 };
let partiallyMatchingItem: { a: string } = item;
let mappedTypeItem: { [key: string]: string } = partiallyMatchingItem;
Or simply:
let item = { a: "Hello World!", b: 1 };
let mappedTypeItem: { [key: string]: string } = item as { a: string };
This is an error waiting to happen, especially when you enumerate through the properties of mappedTypeItem
and you assume that the values of all properties are a string
.
Considering how common structurally typed assignments are in TypeScript, this absolute guarantee does not fit into the system of minimum guarantees generally provided by the type system.
A clean solution would be to make values of "regular" types not assignable to mapped types (if backwards compatibility is required, you could toggle it with a switch in the tsconfig.json
file). At least you should probably avoid these kind of assignments since the type safety provided here is quite weak.