I have declared the following types:
type ExampleA = {
a: string;
}
type ExampleB = {
b: number;
}
type ExampleC = {
c: boolean;
}
type Examples = ExampleA &
ExampleB &
ExampleC;
Then I use the type as follows:
function foo(pattern: { [key: string]: string }) {
console.log(pattern);
}
const bar: Examples = { a: 'foo', b: 1, c: false };
foo(bar);
The typescript compiler is not throwing any error in the invocation of the foo(bar)
method, even though bar:Examples
variable doesn't match the function signature of foo
.
Playground link
Why is typescript not throwing any error? Is this a bug in the compiler?
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.
If you really want to have the error, you can declare Example
as interface, not intersection type. Since 2.2, interface can extend object type (or even intersection type)
type ExampleA = {
a: string;
}
type ExampleB = {
b: number;
}
type ExampleC = {
c: boolean;
}
interface Examples extends ExampleA, ExampleB, ExampleC {
}
function foo(pattern: { [key: string]: string }) {
console.log(pattern);
}
const bar: Examples = { a: 'foo', b: 1, c: false };
foo(bar); // error
Or even this way, to better illustrate the difference between interface and intersection types:
type Examples = ExampleA & // the same as in question
ExampleB &
ExampleC;
interface IExamples extends Examples { // empty interface "collapses" the intersection
}
const bar1: Examples = { a: 'foo', b: 1, c: false };
foo(bar1); // no error
const bar2: IExamples = { a: 'foo', b: 1, c: false };
foo(bar2); // error
One more way to construct object type out of the intersection, as suggested by Titian in the comment, is to use mapped type which is almost, but not quite, identical to its generic parameter:
type Id<T> = { [P in keyof T]: T[P] }
const bar3: Id<ExampleA & ExampleB & ExampleC> = { a: 'foo', b: 1, c: false };
foo(bar3); // error
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With