Skip to content
TypeScript ts objects 4 min read

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.

noUncheckedIndexedAccess is not part of the strict family — 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.

FeatureIndex signatureRecord<K, V>
Open string/number keysYesRecord<string, V>
Finite key unionNoRecord<"a" | "b", V>
Combine with named propsYesNo
Syntax weightHeavierLighter
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 noUncheckedIndexedAccess so indexed reads include | undefined and 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.
Last updated June 29, 2026
Was this helpful?