Intro
Last week, while reading Chapter 7. in Learning TypeScript, I could meet another unfamiliar topic in TypeScript for me - "Index signature".
Then, I realized that this concept might be useful when we declare Type aliases or Interfaces as objects with key-value pairs that are not 100% sure ahead of time.
However, generally speaking, we should avoid using index signatures as much as possible because of the insecure safety of value types for key values (I will talk about that later in this article).
So let's dive into this topic.
What is Index Signature?
Index signature is a special syntax to enable us to assign uncertain key values to object type definitions.
Basically, when we declare Type aliases or Interfaces as objects to apply specified type values to variables, filling out all names of keys and those of value are required to make Type guard in TypeScript work correctly.
Thanks to type definitions following this way, we can prevent referring to wrong values in objects beforehand because TypeScript tells us incorrect value references. Here is a code snippet describing a normal object without index signatures.
// Declare an interface
interface Animal {
species: string;
name: string;
age: number;
}
// Apply this interface to a new constant variable with an object
const animalObject: Animal = {
species: "dog",
name: "John",
age:3,
}
console.log(animalObject.name); // Output: "John"
console.log(animalObject.age); // Output: 3
console.log(animalObject.weight) // Error: Property 'weight' does not exist on type 'Animal'.
But what about this situation? Let's say now I need to memorize all items to buy in the grocery shopping.
I have come up with an idea for storing all shopping items in an object with multiple key-value pairs. Each key represents the genre of items, and its corresponding value is an array that includes the names of items.
However, sometimes it may be necessary to purchase something that was not planned in advance. In such cases, I cannot write down everything I need to buy before going grocery shopping.
But, by taking the power of index signatures, I can create a code sample like this;
// Declare key-value pair with index signature (key name with its type value and wrapping it by the square bracket
interface ItemList {
[gerne: string]: string[]
}
// key values (genre) could be anything unless they are string
// In this case all keys (fruits, vegetables, and meat) are ok as they are all string values.
const shoppingList: ItemList = {
fruits: ['apple', 'banana', 'orange'],
vegetables: ['onion', 'potato'],
meat:['beef', 'pork'],
}
As you can see, the index signature (in this case, [genre: string]) allows any string key even if it is uncertain about the name of the keys unless their value type is string.
Meanwhile, the code snippet implies the code smell in TypeScript and does not secure the safe type guard. So let's move on to the next topic about type safety in index signatures.
Type Safety of Index signature
Although index signatures sound useful when we assign values to an object, we should keep in mind there is no guarantee for type-safe.
In the code sample below, TypeScript does not detect the type error ahead of runtime so we can even access non-exist values by referring to keys defined by index signatures.
interface Song {
[title: string]:{year: number, groupName: string}
}
const myBestSong: Song = {
usAndThem:{year: 1972, groupName: 'Pink Floyd'}
}
myBestSong.year // Type: number
// It is obvious to us that title key does not have the salesRecord value in it (That should be undefined)
myBestSong.salesRecord.toString() // But still no error until the runtime.
console.log(myBestSong.salesRecord.toString()) // Runtime Error: Cannot read properties of undefined (reading 'toString')
This example represents the insecure type safety when using index signatures. For the sake of stronger type guards in TypeScript, we should refrain from utilizing index signatures as much as possible.
An alternative way of index signatures is to make use of Map method.
// Declare a new Map with type
const mapFoo: Map<string,{year: number, groupName: string}> = new Map();
// Add an entry to mapFoo
mapFoo.set('usAndThem', {year: 1972, groupName: 'Pink Floyd'})
// Access to the entry that I added above
// The get method in the Map method returns type with | undefined so we could notice beforehand when we try to access non-exist key values.
mapFoo.get('usAndThem') // Type: (method) Map<string, { year: number; groupName: string; }>.get(key: string): {year: number; groupName: string;} | undefined
mapFoo.get('usAndThem')?.year // Ok
mapFoo.get('usAndThem')?.salesRecord // Error: Property 'salesRecord' does not exist on type '{ year: number; groupName: string; }'
Now we could prevent accessing a key that does not exist in an object. That provides us safer type guards in TypeScript.
Conclusion
An index signature stands for a way to define the shape of fields such as objects if the values of keys in objects are uncertain ahead of time.
A possible case for using index signatures is when we fetch data from API with objects which are not sure about their contents of them beforehand.
At the same time, since index signatures are flexible, meaning any names of values are assignable for keys in objects unless value types are matched. So avoiding the use of index signatures in TypeScript as much as we can is desirable to pursue stronger type protections.
Top comments (1)
So basically an index signature in TypeScript is a way to define types for objects where the keys (or indices) are not known ahead of time, but you do know the types of those keys. These types are given in square brackets.
The term "index signature" seems to come from the fact that this declaration allows you to index an object using dynamic keys, similar to how you would access elements in an array by index.