Index Signatures & Record

Not every object has a fixed list of properties. Dictionaries, lookup tables and caches have keys you don't know in advance. Index signatures and the Record type describe objects like these: "any key of this type, mapping to values of that type".

Index signatures

TS index-signature.ts
interface WordCounts {
  [word: string]: number;
}

const counts: WordCounts = {};
for (const w of "the cat saw the dog".split(" ")) {
  counts[w] = (counts[w] ?? 0) + 1;
}
console.log(counts);

counts.the = "many";
tsc output
error TS2322: Type 'string' is not assignable to type 'number'.

[word: string]: number means any string key is allowed, and every value must be a number. The name word is only a label for readability. Remove the last line and the program prints { the: 2, cat: 1, saw: 1, dog: 1 }.

Missing keys

An index signature says every string key exists, which isn't really true:

TS missing.ts
const stock: { [sku: string]: number } = { "A-1": 4 };

const qty = stock["Z-9"];
console.log(qty);
Output
undefined

qty is typed number but is actually undefined. Turning on noUncheckedIndexedAccess in tsconfig.json makes such reads return number | undefined, which forces a check. For lookups where keys are truly dynamic, a Map is often clearer, since map.get() always returns V | undefined.

Mixing fixed properties and an index signature

An interface can have both, but every fixed property must also match the index signature's value type:

TS mixed.ts
interface Headers {
  [name: string]: string;
  "content-type": string;
  "content-length": number;
}
tsc output
error TS2411: Property '"content-length"' of type 'number' is not assignable to 'string' index type 'string'.

The fix is to widen the index signature (string | number) or keep the dynamic part in a separate nested property.

Number and template-pattern keys

Index signature keys can be string, number, symbol, or a template literal pattern:

TS key-kinds.ts
interface Grid {
  [row: number]: string;
}

interface DataAttrs {
  [attr: `data-${string}`]: string;
}

const grid: Grid = ["#..", ".#.", "..#"];
const attrs: DataAttrs = { "data-id": "42", "data-role": "tab" };

console.log(grid[1], attrs["data-role"]);
Output
.#. tab

The DataAttrs pattern accepts any key starting with data- and rejects anything else. Template literal types are covered in lesson 30.

The Record utility type

Record<Keys, Value> is a built-in shorthand that's usually easier to read than an index signature:

TS record.ts
const capitals: Record<string, string> = {
  France: "Paris",
  Japan: "Tokyo",
};

capitals.Kenya = "Nairobi";
console.log(Object.keys(capitals).length);
Output
3

Record<string, string> is equivalent to { [key: string]: string }.

Record with a fixed set of keys

Record really shines when the keys are a union of literals. Then every key is required — none can be forgotten:

TS record-union.ts
type Status = "draft" | "review" | "published";

const labels: Record<Status, string> = {
  draft: "Draft",
  review: "In review",
};
tsc output
error TS2741: Property 'published' is missing in type '{ draft: string; review: string; }' but required in type 'Record<Status, string>'.

This is perfect for lookup tables keyed by an enum or union: add a new status and the compiler shows every table that needs a new entry. Wrap it in Partial<…> if some keys are allowed to be missing.

Iterating safely

Object.keys() always returns string[], even for a Record with literal keys, because objects can have extra properties at runtime. When you know the keys are exactly the ones in the type, you can use Object.entries or assert the key type:

TS iterate.ts
type Status = "draft" | "review" | "published";
const counts: Record<Status, number> = { draft: 4, review: 1, published: 9 };

for (const [status, n] of Object.entries(counts)) {
  console.log(`${status}: ${n}`);
}

const keys = Object.keys(counts) as Status[];
console.log(keys.map(k => counts[k]).reduce((a, b) => a + b));
Output
draft: 4
review: 1
published: 9
14

Choosing between them

NeedUse
Fixed, known propertiesAn interface or type alias
A table with a known set of keysRecord<"a" | "b", V>
Arbitrary string keysRecord<string, V> or an index signature
Frequent adds/removes, non-string keys, guaranteed key orderMap<K, V>

Try it yourself

Try it yourself
Console output