Index Signatures
Sometimes you do not know an object’s property names ahead of time — a dictionary of settings, a cache keyed by id, or a lookup table built at runtime. An index signature describes such objects by declaring the type of the keys and the type of every value, rather than naming each property. This page covers string and number index signatures, the rules that constrain them, the noUncheckedIndexedAccess flag that makes access safe, and why Record<K, V> is usually the cleaner alternative.
Declaring an index signature
An index signature uses bracket syntax to name a placeholder key and its type, followed by the value type. It says “any property accessed by this key type returns this value type.”
interface StringMap {
[key: string]: string;
}
const headers: StringMap = {
"Content-Type": "application/json",
"Accept": "text/html",
};
headers["X-Custom"] = "value"; // ✅ any string key is allowed
const v: string = headers.anything; // typed as string, even if missing
The key name (key here) is documentation only — what matters is the key type (string) and value type (string). Once an index signature is present, the object accepts arbitrarily many properties of that shape.
String vs number index signatures
You can index by string or by number. Because JavaScript coerces numeric keys to strings at runtime, TypeScript enforces a rule: when both are present, the number-indexed value type must be assignable to the string-indexed one.
interface Mixed {
[key: string]: string | number;
[index: number]: number; // ✅ number ⊆ (string | number)
}
interface Bad {
[key: string]: string;
// ❌ Error: 'number' index type 'number' is not assignable to
// 'string' index type 'string'.
[index: number]: number;
}
Number index signatures model array-like objects, while string signatures model dictionaries. The constraint exists because obj[0] and obj["0"] reach the same property at runtime.
Mixing named and indexed properties
You can combine explicit properties with an index signature, but every named property’s type must be compatible with the index signature’s value type. The index signature acts as a floor that all members must satisfy.
interface Catalog {
name: string;
[sku: string]: string | number; // name is string, so it fits
count: number; // number fits the value type too
}
interface Broken {
// ❌ Error: Property 'enabled' of type 'boolean' is not assignable
// to 'string' index type 'string'.
enabled: boolean;
[key: string]: string;
}
If a named property does not fit, widen the index value type (for example to string | number | boolean) or rethink the model. This rule keeps obj[anyKey] honest with the declared properties.
Safer access with noUncheckedIndexedAccess
By default, indexed access assumes the value exists, which is optimistic — a missing key returns undefined at runtime but is typed as the value type. The noUncheckedIndexedAccess compiler flag fixes this by adding | undefined to every indexed read.
interface Scores {
[name: string]: number;
}
const scores: Scores = { ada: 10 };
// With noUncheckedIndexedAccess enabled:
const s = scores["bo"]; // type: number | undefined
// ❌ Error: 's' is possibly 'undefined'.
const doubled = s * 2;
const safe = scores["bo"] ?? 0; // ✅ handle the missing case
This flag makes lookups type-safe by forcing you to handle absent keys, matching real runtime behavior. Enable it in strict projects that lean on dictionaries.
noUncheckedIndexedAccessis not part of thestrictfamily — you must opt in separately. It is one of the highest-value flags for code that reads from maps, caches, and parsed JSON.
Record vs index signatures
The built-in Record<K, V> expresses the same idea more concisely and, crucially, lets you constrain keys to a finite union — something an index signature cannot do.
| Feature | Index signature | Record<K, V> |
|---|---|---|
| Open string/number keys | Yes | Record<string, V> |
| Finite key union | No | Record<"a" | "b", V> |
| Combine with named props | Yes | No |
| Syntax weight | Heavier | Lighter |
type Role = "admin" | "editor" | "viewer";
// Exhaustive: all three keys required, extra keys rejected
const permissions: Record<Role, boolean> = {
admin: true,
editor: true,
viewer: false,
};
Reach for Record<K, V> for dictionaries, especially with a known set of keys, and keep index signatures for open-ended objects that also carry named properties. See Record for details.
Best Practices
- Use an index signature only for genuinely dynamic keys, not as a way to skip declaring known fields.
- Prefer
Record<K, V>for dictionaries; use a key union to make the set exhaustive. - Enable
noUncheckedIndexedAccessso indexed reads include| undefinedand force handling. - Ensure named properties are assignable to the index signature’s value type.
- Keep the number-indexed value type assignable to the string-indexed one when both exist.
- Default missing lookups with
??instead of asserting the value is present. - Avoid loose
[key: string]: any— it discards type safety across the whole object.