Articles

TypeScript Index Signatures Explained

An index signature lets a TypeScript type describe objects whose keys aren't known in advance. How the syntax works, and when to reach for a Record instead.

Takina Takina · · 4 min read
Code editor showing TypeScript source in a dark theme

An index signature is a TypeScript syntax for typing objects whose property names aren’t known ahead of time — a dictionary of string keys to values, rather than a fixed set of named properties. It looks like { [key: string]: number }, and it’s how you tell the compiler “this object can have any number of properties, but they’re all this shape.”

Most TypeScript objects have a fixed, known set of keys, and an interface or type with named properties is the right tool. Index signatures exist for the other case: config maps, lookup tables, translation dictionaries, anything shaped by data rather than by your code.

The basic syntax

interface WordCount {
  [word: string]: number;
}

const counts: WordCount = {
  apple: 3,
  banana: 1,
};

counts.cherry = 0;       // fine — matches the index signature
counts.cherry = "zero";  // error — must be a number

The [word: string] part is the index signature itself. word is just a label for readability — it doesn’t have to match any real property name. The key type can be string or number (TypeScript also allows symbol and template literal types in newer versions), and the value type is whatever shape every property must conform to.

A numeric index signature is common for array-like structures:

interface StringList {
  [index: number]: string;
}

Index signatures force every property to fit

The catch: once an interface has a string index signature, every other property on that interface must be assignable to the index signature’s value type. This is a common source of confusion:

interface Config {
  [key: string]: string;
  timeout: number; // error: 'number' is not assignable to 'string'
}

TypeScript enforces this because timeout is itself a string key, and the index signature promises that any string key maps to a string. To mix known and unknown properties, widen the index signature’s value type to a union that covers both, or split them into an object with the known keys and a nested map for the rest.

Index signatures vs Record<K, V>

TypeScript’s built-in Record<K, V> utility type is functionally close to an index signature and is usually the better choice when the key type is a plain string or a union of literals:

type Counts = Record<string, number>;
// roughly equivalent to { [key: string]: number }

type Status = "pending" | "active" | "done";
type StatusCounts = Record<Status, number>;
// { pending: number; active: number; done: number }
Index signatureRecord<K, V>
Syntax{ [key: string]: V }Record<K, V>
Key as a literal unionAwkwardNatural — enforces every key is present
Mixing with named propertiesRestricted (all properties must fit)N/A — it’s just an object type
Best forGenuinely open-ended string/number keysA fixed set of keys known at compile time

If you know the exact keys ahead of time — even a large, fixed set — Record with a union of string literals gives you exhaustiveness checking that a plain index signature can’t: TypeScript will flag a missing key. An index signature makes no such promise; it only constrains the shape of whatever keys happen to exist.

noUncheckedIndexedAccess

By default, indexing into a type with an index signature returns the value type directly, with no indication the key might not exist:

const counts: WordCount = { apple: 3 };
const n = counts.banana; // typed as number, but is actually undefined at runtime

This is a real gap between the type system and runtime behavior. The noUncheckedIndexedAccess compiler option closes it by adding | undefined to every index signature access, so counts.banana is typed as number | undefined and you’re forced to handle the missing case. It’s not on by default because it’s a breaking change for a lot of existing code, but it catches a genuine class of bugs and is worth enabling in new projects — see tsconfig.json options for where it lives.

Working with unknown data at the boundary

Index signatures are common when parsing JSON from an external source — an API response, a config file, a .env-style key-value store. Since the actual keys aren’t statically known, resist the temptation to type the whole payload as any; an index signature at least documents the expected value shape. For genuinely untrusted external data, pair this with runtime validation — a schema library, or manual checks — rather than trusting the index signature alone, since TypeScript’s types disappear at runtime and can’t verify anything about the data your program actually receives.

This is the same boundary problem the unknown vs any distinction addresses: an index signature narrows what shape a value should have, but only unknown combined with a runtime check narrows what shape a value actually has.

The takeaway

An index signature — { [key: string]: V } — types objects whose keys aren’t fixed in advance, constraining every property to a common value type. Prefer Record<K, V> when the keys are a known, finite set, since it gets you exhaustiveness checking that a raw index signature can’t provide. And because index signatures describe shape, not presence, turn on noUncheckedIndexedAccess — or validate at runtime — before trusting that an indexed lookup actually returned something.

Takina Takina · · 4 min read

Structural Typing vs Nominal Typing in TypeScript

Structural typing checks shape, not name — TypeScript treats two differently-named types as compatible if their members match, unlike nominal systems.

#TypeScript #JavaScript #Web Development
Takina Takina · · 5 min read

TypeScript Variance Explained

Variance decides when TypeScript accepts Array<Dog> where Array<Animal> is expected — covariant, contravariant, or invariant, explained with examples.

#TypeScript #JavaScript #Web Development
Takina Takina · · 5 min read

Zod vs Yup: TypeScript Schema Validation Compared

Zod infers static TypeScript types directly from its schemas; Yup was built for JavaScript form validation first. How the two approaches differ.

#TypeScript #JavaScript #Web Development