Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Any type can be cast to number map in Typescript

Tags:

typescript

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?

like image 633
margaretkru Avatar asked Oct 15 '25 05:10

margaretkru


1 Answers

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 type number and values string? because anyTypeObj has properties with string keys so I would expect cast to NumberMap 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.

like image 110
Jesse Hallett Avatar answered Oct 18 '25 03:10

Jesse Hallett



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!