Ch. 2 · TypeScript

TypeScript Index Signatures and Constraints

Model keyed collections with index signatures, type the value side, and avoid the loose any that hides real type errors.

~3 min readintermediateupdated Oct 5, 2026

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 number
TypeScript

Walk 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.

More in TypeScript

read ✓TypeScript · hard

TypeScript Abstract Classes and Contracts

Share behavior with abstract classes, enforce required members, and decide when an interface or composition is the better contract.

~2 min readread →
read ✓TypeScript · mid

TypeScript Enums vs Union Types

Compare enums with unions of string literals: runtime cost, serialization, exhaustiveness and which one fits application code.

~3 min readread →
esc