I have an object exampleMap, which implements DictionaryNum<number>, serving as a key value store. I use an object, because Map does not work with e.g. JSON.stringify.

interface DictionaryNum<T> {
    [id: number]: T;
}
let exampleMap = {
    [1]: 1,
}

Sometimes I have to iterate over the whole map, e.g. in the function createQsFromDict() and use the keys and values of the map for further processing. In this case I neglected this, to keep the example simple. However, I iterate over the map using Object.entries(dict).forEach(), because it seems to be the best match for such a task (order does not matter).

function createQsFromDict(dict: DictionaryNum<number>): boolean {
    Object.entries(dict).forEach(
        ([qP, aP]: number[]) => {                           //[qP = "1", aP = 1]  "1"!=1 
            if (qP === 1) {
                return true
            }
        })

    return false
}

let works = createQsFromDict(exampleMap)

The problem is that, although I use type annotations twice (function argument, lambda) and no error is produced, when transpiled, the key is a string, although TypeScript thinks it is a number. Thus, when I use it in a constructor the object will end up with a string in the object. Obviously, this is not what I want. I would expect an implicit conversion or an error.

My question is: What is the best way to get the desired behaviour *. Currently I use parseInt(qP) and remove the type annotations, but this does not seem like optimal solution, considering that this is exactly why I do not want to code in JavaScript.

*keeping:

  • fast access to the individual values
  • fast iteration over keys/values
  • JSON.stringifyable

2 Answers

0
Pedro Arantes On

Looking at Object.entries declaration:

entries<T>(o: { [s: string]: T } | ArrayLike<T>): [string, T][];

Looks like the entries method converts object properties to string by default and you have to convert by yourself inside the callback.

Using == instead === comparator is a solution for you?

"1" === 1 // false
"1" == 1 // true
2
jcalz On

Object.entries(obj) produces an array of [string, any] (or possibly [string, T] for a suitable T in the case that obj is an array of T or a string-keyed dictionary whose values are all T). This is because, even for arrays, JavaScript actually converts all indices (except symbol indices) to strings. So {"1": "hey"} and {1: "hey"} are the same value and have the same type. TypeScript exposes the number index as a convenience to deal with arrays and arraylike objects, but it is technically a lie. And the truth, that array keys are of a type like numericString, would apparently be too complicated for anyone to deal with.

Anyway your question is: why doesn't the compiler do an implicit conversion or throw an error inside the forEach() callback?

Answer: TypeScript doesn't do implicit conversions, which would violate TypeScript Non-Goal #5 about emitting different JS code based on type information... the type system is completely erased before runtime. You're just going to have to convert strings to numbers yourself, or use some other data structure to represent your dictionary (an array of [number, number] pairs, or a Map<number, number>, for example)

As for throwing an error, this is reasonable for you to expect. But, unfortunately, the any type (a.k.a. the "escape hatch" for the type system) is suppressing this. You are taking a value that's expected to be of type [string, any], and treating it like a number[]. TypeScript is happy to treat a tuple type like [A, B] as an Array<A|B>. But [string, any] can be treated like Array<string | any>, which is any[], which can also be treated like number[] without complaint. This is unfortunate for you.

To regain some semblance of type safety, you can use the other overload of Object.entries(dict) by a type assertion:

Object.entries(dict as Record<string, number>)

This will cause Object.entries() to return an Array<[string, number]>, and your code will error unless you actually treat the key as a string. The disconnect is that the standard library doesn't expect someone to pass a non-arraylike numeric-index thing to Object.entries(). If you used an array (something with a length property) it would also have caught the error.

So this is a rather unfortunate series of edge cases that bit you. Sorry. Anyway, hope that helps. Good luck!