Patrick Desjardins Blog
Patrick Desjardins picture from a conference

Index Signature in TypeScript with a Twist

Posted on: 2018-05-22

What if I tell you that the type you specify as the key in your index signature in TypeScript is useless? This is pretty much the case today with version 2.8.3. A historical reason is behind this design and this article will cover the current quirk that you might never notice even if you are using an object with index signature for a while.

Everything is more clear with an example. So here is a code that does not compile in TypeScript:

let x: string = "x"; 
x = 1; // As expected, this line doesn't compile 

The variable is defined to be of type string. I am using a number -- it doesn't compile. At that point, anyone who uses TypeScript agrees that it makes total sense. The user explicitly defines a variable to be a primitive, not only a primitive but a string. If I want a number to be stored in this variable, I'll have to parse the number into a string which can be done in different ways like using x.toString(), using the string constructor (String(x)), or to concatenate the number with an empty string (""+x). Regardless, this is known and accepted since the inception of TypeScript.

An index signature allows an object to have value accessed by an index that can be a string or a number. This is often used in JavaScript to access properties of an object. The pattern in JavaScript to create a dictionary is by using the index signature. The following code is legit in JavaScript.

var x = {}; 
x[123] = "Value in property 123"; 
x["456"] = "Value in property 456"; 
x[true] = "Value in property true"; 

In the end, the type doesn't matter, it becomes a property of the object. For example, you can access the value 123 by using x[123] or x["123"] and same thing for 456 with x[456] or x["456"] and similarly with the boolean value x[true] or x["true"]. Again, so far, so good. Where it starts to become not clear is that TypeScript lets your strongly type the index. First detail: you cannot use boolean. Only a string and a number are allowed. This is no harm because it is not pragmatic to have a boolean. The major twist is that if you define the index to be a number that you can set a string. If you specify the index to be a string, u will be able to set a number. The cherry on top is that you cannot define the index to be an union of a number or a string.

interface Obj { [id: string]: boolean; } 
let y: Obj = {}; 
y["okay"] = true; // string key: legit and compile 
y[123] = false; // number key: legit and compile

interface Obj2 { [id: number]: boolean; } 
let y: Obj2 = {}; 
y["okay"] = true; // string key: legit and compile 
y[123] = false; // number key: legit and compile 

I was bemused by the fact that TypeScript allows me to specify the type and wasn't respecting it. It would be a great way to ensure that no one is using the wrong type, even if at the end it doesn't matter in term of runtime it could matter in term of consistency in your code. As mentioned, the logical signature seems to be with a union since both types are valid.

interface Obj { 
  [id: string | number]: boolean; // Won't compile! 
} 

The documentation of Index Signature mentions this detail:

There are two types of supported index signatures: string and number. It is possible to support both types of indexers, but the type returned from a numeric indexer must be a subtype of the type returned from the string indexer. This is because when indexing with a number, JavaScript will actually convert that to a string before indexing into an object.

This is a quirk in TypeScript because the index signature did not exist at the time the index signature was born. This reason is not documented else than in this thread I stared in the official TypeScript Github repository. Ryan Cavanaugh was patient enough to give more context around the past decision that results to the current behavior. Changing the behavior to allow a union is not yet considered by fear of confusing developers. Being more strict in term of verifying that the index is really respecting the index type can break actual code which mean that a transition phase with a deprecate warning would be required. TypeScript's team doesn't believe it worth the change at the moment and kudos to them to keep backward compatibility in the evolution of the language. However, this inconsistency of syntax where the user clearly identify something in a specific time and result into behaving differently is a long term issue that should be addressed. Hopefully you understand that the type of the index is useless at the moment and you can be free to use whatever you want... but not a union of them.