TypeScript Index Signatures

An index signature is TypeScript’s way of describing an object whose exact property names aren’t known ahead of time, but whose value types are. Instead of listing every possible key in an interface, you tell the compiler that any key of a certain kind maps to a value of a certain type, and TypeScript enforces that rule everywhere the object is used. This is the tool you reach for when modeling dictionaries, configuration maps, JSON-derived data, or anything keyed dynamically at runtime.

Overview: How Index Signatures Work

Normally, an interface lists a fixed set of named properties: { id: number; name: string }. That works when you know every key in advance. But many real objects don’t have a fixed key set — think of a dictionary mapping product SKUs to prices, or a translation table mapping message keys to strings. For these, TypeScript lets you add an index signature to an interface or type: a special property definition of the form [key: string]: ValueType that describes every property that might exist on the object, rather than one specific property.

Once an index signature is present, TypeScript uses it in two directions. First, when you assign an object literal to that type, every property in the literal (aside from any explicitly named properties) is checked against the index signature’s value type. Second, when you read a property off a value of that type using bracket or dot notation with an unknown key, TypeScript infers the index signature’s value type as the result — even for keys that don’t actually exist on the object at runtime. That second point is important and comes back in the Common Mistakes section: an index signature is a promise you make to the compiler, not a runtime guarantee.

TypeScript supports two kinds of index signature keys: string and number. A string index signature applies to every property access, because in JavaScript all object keys are ultimately strings (or symbols) under the hood — even obj[0] is really obj["0"]. A number index signature is narrower: it only applies to properties accessed with a numeric key, and it’s mainly used to model array-like or tuple-like structures. Because of this relationship, if an interface declares both, the type returned by the number index signature must be assignable to the type returned by the string index signature — TypeScript won’t let the numeric view promise something the string view can’t back up.

It’s also worth relating index signatures to the Record<K, V> utility type, which you’ll see constantly in TypeScript code. Record<string, V> is essentially sugar for an interface with a single index signature { [key: string]: V }. Reach for Record when you just need a plain dictionary type inline; reach for an explicit index signature inside an interface when you also need named, required properties alongside the dynamic ones, or when you’re extending/merging interfaces.

Finally, remember that none of this exists at runtime. Index signatures, like all TypeScript types, are erased during compilation. The compiled JavaScript is just a plain object with whatever properties you actually put on it — there’s no metadata, no runtime check, and no enforcement once the code is running. All the safety an index signature gives you is a compile-time contract between you and the type checker.

Syntax

interface TypeName {
  knownProp: SomeType;
  [indexName: string | number]: ValueType;
}
  • indexName — an arbitrary label for the key parameter (purely documentation; you never reference it directly). Common choices are key, index, or a domain word like sku.
  • string | number — the index signature must use exactly one of these two key types (you write one or the other, not literally both in one signature).
  • ValueType — the type every matching property’s value must have. This can be a union, e.g. string | number, to allow several kinds of values.
  • knownProp — you can freely mix explicitly named, required properties with an index signature, as long as each named property’s type is assignable to the index signature’s value type (for string index signatures) or is not a numeric key name (for number index signatures).
  • readonly — prefix the signature with readonly [key: string]: ValueType to prevent writes through any key of that object’s type.

Examples

Example 1: A basic string-keyed dictionary

interface StringDictionary {
  [key: string]: string;
}

const colors: StringDictionary = {
  primary: "blue",
  secondary: "green",
};

colors.tertiary = "red";

console.log(colors.primary);
console.log(colors["tertiary"]);
console.log(Object.keys(colors));

Output:

blue
red
[ 'primary', 'secondary', 'tertiary' ]

The index signature [key: string]: string says any property name is allowed as long as its value is a string. TypeScript checks the initial object literal against that rule, and it also allows adding new properties later (colors.tertiary = "red") since that assignment also satisfies the signature. Reading any string-keyed property, known or not, resolves to type string.

Example 2: A number index signature for array-like data

interface RosterEntry {
  name: string;
  role: string;
}

interface Roster {
  [index: number]: RosterEntry;
  length: number;
}

const team: Roster = {
  0: { name: "Ava", role: "Lead" },
  1: { name: "Ben", role: "Engineer" },
  length: 2,
};

function printRoster(roster: Roster): void {
  for (let i = 0; i < roster.length; i++) {
    console.log(`${i}: ${roster[i].name} (${roster[i].role})`);
  }
}

printRoster(team);

Output:

0: Ava (Lead)
1: Ben (Engineer)

Here Roster mixes a number index signature with a normal named property, length. Because length isn't accessed as a numeric key, it doesn't have to match the index signature's value type — only properties actually keyed by number are constrained to be RosterEntry. This pattern is how TypeScript's own built-in Array and ArrayLike types are shaped internally.

Example 3: Combining known properties with a flexible value union

interface UserRecord {
  id: number;
  name: string;
  [key: string]: string | number;
}

const user: UserRecord = {
  id: 101,
  name: "Priya",
  role: "admin",
  loginCount: 12,
};

function describeUser(record: UserRecord): string {
  const extras = Object.keys(record)
    .filter((key) => key !== "id" && key !== "name")
    .map((key) => `${key}=${record[key]}`)
    .join(", ");
  return `${record.name} (#${record.id}) — ${extras}`;
}

console.log(describeUser(user));

Output:

Priya (#101) — role=admin, loginCount=12

This is the pattern you'll use most in real code: a handful of required, known fields (id, name) plus an open-ended index signature for whatever extra data tags along. Notice the union string | number on the index signature — both id: number and name: string must be individually assignable to that union, which they are.

Under the Hood: What the Compiler Checks

When you write an object literal against a type with an index signature, the checker walks every property in the literal. Named properties are checked against their own declared type first; any remaining properties are checked against the index signature's value type. When you later read obj[someStringVariable], the compiler doesn't know the concrete value of someStringVariable, so it can't look up a specific named property — it falls back to the index signature and returns its value type as the result type of the expression. Crucially, this lookup is purely a type-level inference; there is no runtime check that the key actually exists. After compilation, all of this vanishes: the emitted JavaScript is a plain object literal or plain property access, with zero trace of the interface, the index signature, or any type annotation. This is why a variable typed as returning string from an index signature can, at runtime, actually be undefined if the key was never set — the type system's promise doesn't reach into the running program.

Common Mistakes

Mistake 1: A named property's type doesn't match the index signature

All named properties must be assignable to the index signature's value type, because at runtime a named property is just another key. This fails to compile:

interface Config {
  name: string;
  [key: string]: number;
}

TypeScript reports: Property 'name' of type 'string' is not assignable to 'string' index type 'number'. The fix is to widen the index signature's value type to a union that also covers the named property:

interface Config {
  name: string;
  [key: string]: string | number;
}

const settings: Config = {
  name: "production",
  timeout: 3000,
  retries: 3,
};

console.log(settings.name, settings.timeout, settings.retries);

Output:

production 3000 3

Mistake 2: Trusting the index signature's type for keys that might not exist

An index signature says "if this key exists, its value has this type" — it does not say the key exists. Accessing an unknown key still type-checks and still returns the declared value type, even though the runtime value is undefined:

interface Inventory {
  [item: string]: number;
}

const stock: Inventory = { apples: 10, bananas: 5 };

function report(item: string): string {
  const count = stock[item];
  return `${item}: ${count}`;
}

console.log(report("apples"));
console.log(report("oranges"));

Output:

apples: 10
oranges: undefined

Nothing here fails to compile, which is exactly the trap: count is typed as number, but for "oranges" it's actually undefined at runtime. Guard against this by checking key presence explicitly before trusting the value:

interface Inventory {
  [item: string]: number;
}

const stock: Inventory = { apples: 10, bananas: 5 };

function report(item: string): string {
  if (!(item in stock)) {
    return `${item}: not in stock`;
  }
  const count = stock[item];
  return `${item}: ${count}`;
}

console.log(report("apples"));
console.log(report("oranges"));

Output:

apples: 10
oranges: not in stock

For even stronger safety, enable the noUncheckedIndexedAccess compiler flag, which makes every index-signature read return ValueType | undefined automatically, forcing you to narrow before use.

Best Practices

  • Prefer Record<string, V> for a plain dictionary type with no named properties — it's shorter and reads more clearly than a one-line index signature interface.
  • Use an explicit index signature inside an interface only when you also need required named properties alongside the dynamic ones.
  • Widen the index signature's value type to a union (e.g. string | number) whenever named properties of different types need to coexist with it.
  • Turn on noUncheckedIndexedAccess in tsconfig.json for any codebase that leans heavily on dictionaries — it closes the "looks safe, is actually undefined" gap from Mistake 2.
  • Use readonly [key: string]: ValueType for lookup tables that should never be mutated after creation, such as constant maps.
  • Prefer a number index signature only for genuinely array-like structures; for everyday dictionaries keyed by identifiers, a string index signature is almost always the right choice, since JS object keys are strings anyway.

Practice Exercises

  • Define an interface PriceList with a string index signature mapping product names to numeric prices, then write a function that sums the total value of a PriceList object using Object.values.
  • Write an interface HttpHeaders with a string index signature whose value type is string, plus a required named property contentType: string. Explain (in a comment) why this compiles without widening the index signature's value type.
  • Take the Inventory interface from the Common Mistakes section and write a function restock(inv: Inventory, item: string, amount: number): void that adds amount to the existing count for item, treating a missing item as starting at zero.

Summary

  • An index signature, written [key: string]: ValueType or [key: number]: ValueType, lets an interface or type describe objects with dynamic, unknown-in-advance property names.
  • Named properties on the same interface must have types assignable to the index signature's value type, since every named property is also a key at runtime.
  • A number index signature only applies to numeric keys; its value type must be assignable to any coexisting string index signature's value type.
  • Record<K, V> is largely sugar for a single-index-signature type and is the simpler choice for plain dictionaries.
  • Reading an unknown key still type-checks and returns the declared value type — the index signature is a compile-time promise, not a runtime guarantee, so unknown keys can still be undefined at runtime.
  • All of this is erased at compile time; the emitted JavaScript has no trace of the index signature, only the plain object and its actual properties.