Index signatures
Type objects with unknown keys but known value types, such as dictionaries and lookup tables.
interface StringArray { [index: number]: string }
interface Dictionary { [key: string]: number }
interface ReadonlyDict { readonly [key: string]: string }
type Prefixed = { [key: `data-${string}`]: string }- Added in TypeScript 1.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$key name | identifier | Documentation only. `[key: string]` and `[whatever: string]` are the same type; the name appears in editor hints. |
$key type | string | number | symbol | template pattern | Only these are permitted, plus unions consisting solely of them. When both a number and a string index exist, the number index's value type must be a subtype of the string index's. |
$value type | T | The type of every value reached through the index, and the type every explicitly named property must be assignable to. |
$readonly modifieroptional | readonly [k: string]: T | Permits reading through the index but forbids writing. |
Errors
tsc reports "Property 'name' of type 'string' is not assignable to 'string' index type 'number'" when a named property conflicts with the index signature, "'number' index type 'Animal' is not assignable to 'string' index type 'Dog'" for incompatible dual indexers, and "Index signature in type 'X' only permits reading" for writes to a readonly index.
Changelog
- PHP 4.1`noUncheckedIndexedAccess` added: reads through an index signature become `T | undefined`, forcing a guard before use.
- PHP 4.4`symbol` and template string pattern index signatures allowed, so `[key: `data-${string}`]: string` became expressible.
An index signature describes an object whose keys you do not know ahead of time but whose values all share a type. { [key: string]: number } is the dictionary pattern: any string key, always a number back.
Named properties must conform
Because a string index declares that obj.property is also reachable as obj["property"], every explicitly declared property has to be assignable to the index's value type. Adding name: string to a [key: string]: number interface is an error. Widen the index to a union (number | string) if you really need mixed values — though at that point an explicit interface, or an intersection of a known shape with a dictionary, usually models the data better.
The read that lies
An index signature says what the value type is if the key exists. It does not promise the key exists. By default dict["missing"] is typed number, compiles fine, and hands you undefined at runtime — a classic source of "cannot read properties of undefined". Turning on noUncheckedIndexedAccess makes every such read T | undefined and forces a check.
Prefer Record for simple cases
Record<string, T> is the same thing with less ceremony, and Record<"a" | "b", T> gives you a closed key set an index signature cannot express.
Example
When to use it
- Type a cache or lookup table keyed by an id you cannot enumerate at compile time.
- Model an environment or config bag of string values with a few known keys typed alongside.
- Type the result of a grouping operation, where the keys come from the data.
- Restrict keys to a naming convention such as `data-*` with a template pattern index signature.
Frequently asked questions
Why can't I add a string property to my `[key: string]: number` interface?
Because a string index signature promises that every property is reachable through the index with that value type, so each named property must be assignable to it. Widen the index to `number | string`, or intersect a precisely-typed shape with a separate dictionary type.
Should I use an index signature or `Record`?
`Record<string, T>` is shorthand for the same index signature and is usually clearer. Use an explicit index signature when you need the `readonly` modifier, a template-pattern key, or named properties declared alongside. Use `Record` with a union key when the key set is closed and you want missing keys reported.
Why does reading an unknown key not error?
By default TypeScript assumes any key you index with is present, so the read is typed `T` even when the value is `undefined` at runtime. Turn on `noUncheckedIndexedAccess` (TypeScript 4.1+) to get `T | undefined` and be forced to guard.
More examples
Grouping into a dictionary, then exposing it readonly
Prints "classic -> Emma", "scifi -> Dune, Neuromancer", then "missing key: undefined". Keys are sorted so the output is stable. The build phase uses a mutable `Record`, and the finished value is handed out as a readonly index signature, so callers can read but not write. The last line is the honest demonstration that an index signature never promises a key exists.
type Grouped = { readonly [genre: string]: readonly string[] };
const books = [
{ title: "Dune", genre: "scifi" },
{ title: "Emma", genre: "classic" },
{ title: "Neuromancer", genre: "scifi" },
];
const byGenre: Record<string, string[]> = {};
for (const b of books) (byGenre[b.genre] ??= []).push(b.title);
const grouped: Grouped = byGenre; // build mutable, expose readonly
for (const genre of Object.keys(grouped).sort()) {
console.log(genre, "->", grouped[genre].join(", "));
}
console.log("missing key:", grouped["poetry"]);A known shape plus an open bag of extras
Prints "localhost:8080", "verbose: true", then the four keys in sorted order. Declaring `host: string` inside a single interface with `[flag: string]: unknown` would be legal here (everything is assignable to `unknown`) but would lose the precise types on reads. Intersecting a known shape with a dictionary keeps `config.host` a `string` while still permitting arbitrary extra keys, which come back as `unknown` and must be narrowed.
type Config = { host: string; port: number } & { [flag: string]: unknown };
const config: Config = { host: "localhost", port: 8080, verbose: true, retries: 3 };
console.log(`${config.host}:${config.port}`); // typed string and number
const verbose = config["verbose"]; // unknown — must be narrowed
if (typeof verbose === "boolean") console.log("verbose:", verbose);
for (const key of Object.keys(config).sort()) {
console.log(key, "=", JSON.stringify(config[key]));
}
Discussion