Skip to content
Theory · Interfaces and structural typing · lesson 9 of 12

Index signatures

A storekeeper keeps a stock book. Each line has the name of a product on the left and how much of it is in the warehouse on the right: "apples: 5", "pears: 2". Nobody knows in advance which products arrive tomorrow, so the book has no ready-made lines with names. Only one rule is known: on the right there is always a number.

An interface with listed properties cannot describe such a book. interface Stock { apple: number; pear: number } leaves no room for plums: the type has no property plum. You need a way to say "the key is any string, the value is a number". That is what an index signature is for:

TypeScript
interface Stock {
    [item: string]: number;
}

const stock: Stock = { apple: 5, pear: 2 };
stock.plum = 7;
stock["kiwi"] = 1;

console.log(stock.apple, stock["plum"]); // 5 7
console.log(Object.keys(stock).length); // 4

Let's go through the example line by line:

  1. [item: string]: number means an object of type Stock may have any number of properties with string keys, and each of them holds a number.
  2. The literal sets two products, and both values are numbers.
  3. stock.plum = 7 and stock["kiwi"] = 1 add new keys. tsc does not object: the key is a string, the value is a number.
  4. The program prints 5 7, then 4: the object has four keys.

How to read the signature

[item: string]: number has three parts:

  • the square brackets mean "any key", not one particular property;
  • item: string is the key's name and type; the name is there for the reader and does not affect the check;
  • : number after the brackets is the type of the value under any key.

The signature can also be written right in an annotation, with no interface: const stock: { [item: string]: number } = {};.

tsc checks the value's type on every write. The line stock.pear = "two" fails: error TS2322, a string cannot be written where a number is expected.

A missing key

The signature promises a number under any key. But a product may be missing:

TypeScript
interface Stock {
    [item: string]: number;
}

const stock: Stock = { apple: 5 };
const count = stock["mango"];
console.log(count); // undefined

Hover over count, and the editor shows the type number, not number | undefined. The object has no key mango, so at run time count holds undefined, and tsc gives no warning about it.

Here the stock-book analogy stops working. The storekeeper sees that there is no "mango" line, while TypeScript goes by the signature and assumes there is a number under every key.

The compiler has an option noUncheckedIndexedAccess. With it, reading by a key gives number | undefined, and the value has to be checked, like an optional property. It is not part of strict mode; you turn it on separately.

Map instead of an object

JavaScript also has Map for dictionaries. Its get method says honestly that the key may be missing:

TypeScript
const stock = new Map([["apple", 5]]);
const count = stock.get("mango");
console.log(count ?? 0); // 0

The editor shows stock as Map<string, number>: the keys are strings, the values are numbers. count has the type number | undefined, so it cannot be used without a check, and ?? 0 puts in a zero. Map is handy when keys are added and removed as the program runs. A plain object with an index signature shows up where the data arrives as a ready object, for example from JSON.

Common mistake

A key was read without a check, trusting the type number:

TypeScript
interface Stock {
    [item: string]: number;
}

const stock: Stock = { apple: 5 };
console.log(stock["mango"].toFixed(1)); // ✗
TEXT
TypeError: Cannot read properties of undefined (reading 'toFixed')

tsc stays silent: to it, stock["mango"] is a number. The program crashes at run time, because stock["mango"] holds undefined. If a key may be missing, check it, for example with in: inside if ("mango" in stock) the value is there.

Key points

  • [item: string]: number describes a dictionary: any string key, a number under each.
  • The value's type is checked on write: a string cannot go into such a dictionary.
  • tsc types a read of a missing key as number, although at run time it is undefined; the noUncheckedIndexedAccess option changes that.
  • Map's get method returns number | undefined, and tsc itself demands the check.