Object index key type in Typescript

TypescriptTypescript Generics

Typescript Problem Overview


I defined my generic type as

interface IDictionary<TValue> {
    [key: string|number]: TValue;
}

But TSLint's complaining. How am I supposed to define an object index type that can have either as key? I tried these as well but no luck.

interface IDictionary<TKey, TValue> {
    [key: TKey]: TValue;
}

interface IDictionary<TKey extends string|number, TValue> {
    [key: TKey]: TValue;
}

type IndexKey = string | number;

interface IDictionary<TValue> {
    [key: IndexKey]: TValue;
}

interface IDictionary<TKey extends IndexKey, TValue> {
    [key: TKey]: TValue;
}

None of the above work.

So how then?

Typescript Solutions


Solution 1 - Typescript

You can achieve that just by using a IDictionary<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.

Solution 2 - Typescript

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.

Solution 3 - Typescript

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRobert KoritnikView Question on Stackoverflow
Solution 1 - TypescriptNickeatView Answer on Stackoverflow
Solution 2 - TypescriptNitzan TomerView Answer on Stackoverflow
Solution 3 - TypescriptSandy GiffordView Answer on Stackoverflow