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:
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); // 4Let's go through the example line by line:
[item: string]: numbermeans an object of typeStockmay have any number of properties with string keys, and each of them holds a number.- The literal sets two products, and both values are numbers.
stock.plum = 7andstock["kiwi"] = 1add new keys. tsc does not object: the key is a string, the value is a number.- The program prints
5 7, then4: 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: stringis the key's name and type; the name is there for the reader and does not affect the check;: numberafter 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:
interface Stock {
[item: string]: number;
}
const stock: Stock = { apple: 5 };
const count = stock["mango"];
console.log(count); // undefinedHover 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:
const stock = new Map([["apple", 5]]);
const count = stock.get("mango");
console.log(count ?? 0); // 0The 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:
interface Stock {
[item: string]: number;
}
const stock: Stock = { apple: 5 };
console.log(stock["mango"].toFixed(1)); // ✗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]: numberdescribes 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 isundefined; thenoUncheckedIndexedAccessoption changes that. Map'sgetmethod returnsnumber | undefined, and tsc itself demands the check.