An index signature describes an object with an open set of keys, such as { [key: string]: User }. It is the natural type for a dictionary, but an unconstrained any value or a careless string index lets mistakes through, so the constraint and the value type both matter.
Before you start
You should be comfortable with interfaces, Record, and union types. This article covers index signatures and the constraints TypeScript enforces around them.
Step-by-step walkthrough
Step 1: Constrain the key type
A string index signature accepts any string key, which matches a dictionary but also accepts typos. When keys come from a known set, prefer a mapped type over a union, such as Record<UserId, User> or { [K in Status]: Handler }, so lookups are checked against real keys.
Step 2: Type the value precisely
{ [key: string]: any } disables checking on every read. Use the real value type, or unknown at a boundary you must validate. A typed index such as { [key: string]: number } ensures every stored value has the right shape.
Step 3: Reconcile index signatures with fixed properties
If an interface has both a string index and a named property, the named property’s type must be assignable to the index value type. For example, { [key: string]: number; count: string } is an error because count is not a number. This rule keeps lookups consistent with the declared value type.
Worked scenario
The index value type is enforced on every write and read.
interface Scoreboard {
[playerId: string]: number;
}
const scores: Scoreboard = {};
scores['p-1'] = 10;
const total: number = scores['p-2'] ?? 0;
// scores['p-3'] = 'high'; // error: string is not assignable to numberWalk through the example
The attempt to store a string is rejected because the index value must be a number. Reading a missing key is typed number, not number | undefined, which is a useful reminder that index signatures do not add undefined unless noUncheckedIndexedAccess is enabled — with that flag on, every lookup becomes number | undefined and you must handle absence.
Common mistake
Using { [key: string]: any } to silence errors, which propagates any to every property access and defeats the checker. Another mistake is assuming a lookup can be missing: without noUncheckedIndexedAccess, scores['missing'] is typed number, so a runtime undefined flows into code that never expected it.
Verify the behavior
Assign a wrong value type and confirm the compiler rejects it. Add a named property with a conflicting type and read the index-signature error. Turn on noUncheckedIndexedAccess and confirm lookups become optional, then handle the undefined. Use a Record<Union, T> and confirm an invalid key is rejected.
Interview exercise
You model feature flags as { [name: string]: boolean }. A teammate writes flags['darkmodee'] and expects a compile error. Why is there none?
Answer and reasoning
A string index signature declares that every string key is valid, so any spelling type-checks and returns boolean (or boolean | undefined with noUncheckedIndexedAccess). To catch typos, narrow the key type to the known flag names with a union or a mapped record, for example Record<FlagName, boolean>, so only real names are accepted.
Continue learning
See the related access rules in keyof and indexed access and utility types from scratch. Read the TypeScript index signatures handbook and try the TypeScript interview questions.