Exploring [key:string]: any in TypeScript
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
😂
Resources
The code snippets used in this article is also available at this TypeScript playground.