Exploring [key:string]: any in TypeScript

May 29, 2021

With this series I intend to note down some of the confusion and quirky stuff that I encountered out in the wild. So, today I am going to start with this snippet in TypeScript.

Motivation

interface CustomState { value: { [key:string]: any } } const defaultState : CustomState = { value: {} } const reducer = (state: CustomState, action: { type: string }): CustomState => { if (action.type === 'reset') { return { value: [] } } else { return { ...state } } }

The CustomState declared at the start includes a property called value, which is an object with key-value pairs of the form string - any. The defaultState variable contains an (empty) object conforming to the interface declared above, which is perfectly normal.

The thing that caught me off-guard is in the reducer. The reducer function is supposed to reset the state by clearing out the value property. However, notice here that an array [] is used, instead of {}.

I thought the change from type object to type array is pretty drastic, especially if I compare it to Java (Changing from a HashMap to an ArrayList just like that? Is this even allowed?). The strangest part of this was that TypeScript had no qualms about this at all. No curly lines nor compiler warnings.


Exploration

The first thing I did was to find out whether the interface was declared correctly, i.e. is it declaring that value contains an object or an array. This led me to review the definition of index signature.

Index Signature

Index signature is a way to describe the types of possible values. Borrowing the examples used in the official TypeScript docs:

interface StringArray { [index: number]: string; } const myArray: StringArray = getStringArray(); const secondItem = myArray[1]; // secondItem is of type string

The syntax to declare the index signature might seem strange at first. It looks like declaring an object with {} but in the above example, it is used for declaring an interface for an array. For comparison, the way to declare an object interface looks like this:

interface PaintOptions { xPos: number; yPos: number; }

In the TypeScript documentation example, index signature is used to describe an array. However, there is also a line below that says:

While string index signatures are a powerful way to describe the “dictionary” pattern, they also enforce that all properties match their return type.

Doing a bit more research would point me to other examples of how index signatures are also applicable to objects:

interface NumberDictionary { [index: string]: number; length: number; width: number; }

So in the case of CustomState, both the following usage are correct:

const arrayExample:CustomState = { value: [{val: 1}] } const objectExample:CustomState = { value: {val: 1} }

Array VS Object

The second thing I checked was that since {} could be replaced with [], are arrays and objects, besides what we already know about the different use cases, the same thing in JavaScript/TypeScript? Without going too deep into this question, we can make an observation with console log :

console.log(typeof []) // "object" console.log(typeof {}) // "object"

Stack Overflow?

The last bit of things of interest came up when I started to draft examples for this article and encountered this stack overflow question. Essentially, the person had an issue with Index signature of object type implicitly has an 'any' type. Scrolling further down, an proposed answer had something similar to my initial example:

type ISomeType = {[key: string]: any}; let someObject: ISomeType = { firstKey: 'firstValue', secondKey: 'secondValue', thirdKey: 'thirdValue' }; let key: string = 'secondKey'; let secondValue: string = someObject[key];

In fact to add on, the declaration of ISomeType allows for the following to work as well:

type ISomeType = {[key: string]: any}; // My additional example let someArray: ISomeType = [ {firstKey: 'firstValue'}, {secondKey: 'secondValue'}, {thirdKey: 'thirdValue'} ] let newkey: string = 'secondKey'; let newSecondValue: string = someArray[newkey];

But, if the use of any has been replaced, the whole thing would break:

// Note the change in the type and therefore the error! type ISomeTypeA = {[key: string]: string}; let someObjectA: ISomeTypeA = { firstKey: 'firstValue', secondKey: 'secondValue', thirdKey: 'thirdValue' }; let keyA: string = 'secondKey'; let secondValueA: string = someObjectA[keyA]; // My additional example let someArrayA: ISomeTypeA = [ // Error: Type '{ firstKey: string; }' is not {firstKey: 'firstValue'}, // assignable to type 'string'. {secondKey: 'secondValue'}, {thirdKey: 'thirdValue'} ] let newkeyA: string = 'secondKey'; let newSecondValueA: string = someArrayA[newkeyA];

Potential moral of the story? Don’t use any emoji-joy


Resources

The code snippets used in this article is also available at this TypeScript playground.



Profile picture

Written by Liu Yongliang who lives in Singapore. Also on Dev.to, LinkedIn, GitHub

© Copyright 2022 Liu Yongliang