Object index key type in Typescript Object index key type in Typescript typescript typescript

Object index key type in Typescript


You can achieve that just by using aIDictionary<TValue> { [key: string]: TValue } since numeric values will be automatically converted to string.

Here is an example of usage:

interface IDictionary<TValue> {    [id: string]: TValue;}class Test {    private dictionary: IDictionary<string>;    constructor() {       this.dictionary = {}       this.dictionary[9] = "numeric-index";       this.dictionary["10"] = "string-index"       console.log(this.dictionary["9"], this.dictionary[10]);    }}// result => "numeric-index string-index"

As you can see string and numeric indices are interchangeable.


In javascript the keys of object can only be strings (and in es6 symbols as well).
If you pass a number it gets converted into a string:

let o = {};o[3] = "three";console.log(Object.keys(o)); // ["3"]

As you can see, you always get { [key: string]: TValue; }.

Typescript lets you define a map like so with numbers as keys:

type Dict = { [key: number]: string };

And the compiler will check that when assigning values you always pass a number as a key, but in runtime the keys in the object will be strings.

So you can either have { [key: number]: string } or { [key: string]: string } but not a union of string | number because of the following:

let d = {} as IDictionary<string>;d[3] = "1st three";d["3"] = "2nd three";

You might expect d to have two different entries here, but in fact there's just one.

What you can do, is use a Map:

let m = new Map<number|string, string>();m.set(3, "1st three");m.set("3", "2nd three");

Here you will have two different entries.


Even though object keys are always strings under the hood, and typing indexers as strings covers numbers, sometimes you want a function to be aware of the keys of objects being passed into it. Consider this mapping function which works like Array.map but with objects:

function map<T>(obj: Object, callback: (key: string, value: any) => T): T[] {    // ...}

key is restricted to being a string, and value is entirely untyped. Probably fine 9 out of 10 times, but we can do better. Let's say we wanted to do something silly like this:

const obj: {[key: number]: string} = { 1: "hello", 2: "world", 3: "foo", 4: "bar" };map(obj, (key, value) => `${key / 2} ${value}`);// error: The left-hand side of an arithmetic operation must be of type 'any', 'number' or an enum type.

We can't perform any arithmetic operations on key without first casting it to a number (remember: "3" / 2 is valid in JS and resolves to a number). We can get around this with a little bit of tricky typing on our map function:

function map<S, T>(obj: S, callback: (key: keyof S, value: S[keyof S]) => T): T[] {    return Object.keys(obj).map(key => callback(key as any, (obj as any)[key]));}

Here, we use the generic S to type our object, and look up key and value types directly from that. If your object is typed using generic indexers and values, keyof S and S[keyof S] will resolve to constant types. If you pass in an object with explicate properties, keyof S will be restricted to the property names and S[keyof S] will be restricted to the property value types.