It seems like typescript allows casting any type to a map with key of type number
. Here is an example code:
type NumberMap = {
[key: number]: string
}
type AnyType = {
foo: string
bar: boolean[]
}
const anyTypeObj: AnyType = { foo: "foo", bar: [false] }
const numberMap: NumberMap = anyTypeObj
I expect the last line to give me a type error when trying to assign AnyType
to NumberMap
, but it doesn't. However, assigning an object literal directly to a variable of type NumberMap
works as expected and causes a type error:
const numberMap: NumberMap = { foo: "foo", bar: [false] }
Here is a link with the above code on typescript playground.
Why does typescript allow casting objects of any type to NumberMap
?
Is there anything special about the type { [key: number]: string }
that makes it behave this way?
Typescript does some extra checking when an object literal is assigned to a varibale. This is called excess property checking. The idea is that an object literal that is assigned to a variable whose type does not have exactly the same properties is likely to be a mistake. On the other hand reassignments and argument values that are not exact matches are common in Javascript, so inexact assignments are allowed when the right-hand expression is not an object literal as long as the value's type is compatible with the variable's type.
In your example anyTypeObj
is compatible with NumberMap
. You can treat anyTypeObj
as an empty NumberMap
. The contract for NumberMap
specifies that if you access a property by a numeric key the result will be a string. anyTypeObj
has no numeric keys, so there is no case where that will not be true.
Note that once you have assigned anyTypeObj
to numberMap
you should not be able to access the foo
or bar
properties via numberMap
, because the NumberMap
type does not declare those properties.
Edit: margaretkru followed up with this question:
why doesn't
{ [key: number]: string }
enforce that all keys on an object must be of typenumber
and valuesstring
? becauseanyTypeObj
has properties withstring
keys so I would expect cast toNumberMap
fail in that case.
My explanation is that a variable can accept any value with a type that is compatible with the variable's type. I think of compatibility in terms of subtypes. For example:
const a: Animal = new Cow()
That works if Cow
is a subclass of Animal
even if Cow
has properties that Animal
does not.
In object-oriented languages types are either classes or interfaces, and type B
is only considered a subtype of A
if B
explicitly extends or implements A
. But some languages (including Typescript) take a more general view. The most general definition of "type" is a description of a set of possible values that satisfy certain criteria. B
is a subtype of A
if B
describes a set of values that is a subset of the set that A
describes. It does not matter whether or not B
names A
in its definition.
NumberMap
describes the set of all objects whose numeric properties all have type string
.
AnyType
is a subtype of NumberMap
because all of its numeric properties have type string
. (This is trivially true because AnyType
has no numeric properties). The string properties on AnyType
do interfere with NumberMap
's contract because NumberMap
does not say anything about string properties.
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