Skip to content
Laviyx Learn

Try

TypeScript course menu

TypeScript Index Signatures and Record: Typing Dictionaries and Lookup Objects

Learn to type objects with dynamic keys in TypeScript: index signatures [key: string], Record<K, V> for dictionaries and lookup tables, Record with literal key unions, noUncheckedIndexedAccess, and when to use Map instead.

Intermediate6 min readUpdated
On this page

Some objects do not have a fixed set of properties: a price list, word counts, translations, or settings keyed by user id. TypeScript types these with index signatures or the Record utility type.

Index signatures#

TypeScript
type PriceList = {
  [item: string]: number;
};

const prices: PriceList = { tea: 20, coffee: 35 };
prices.juice = 50;           // any string key is allowed
prices["green tea"] = 25;

console.log(Object.keys(prices).length, prices.coffee);
Output
4 35

[item: string]: number means "any string key maps to a number". The name item is just for documentation. Values must still match:

TypeScript
type PriceList = { [item: string]: number };
const prices: PriceList = { tea: 20 };
prices.juice = "fifty";
Compiler error
error TS2322: Type 'string' is not assignable to type 'number'.

You can mix fixed properties with an index signature, as long as the fixed ones match the signature's value type:

TypeScript
type Translations = {
  [key: string]: string;
  language: string;
};

const hindi: Translations = { language: "hi", hello: "नमस्ते", thanks: "धन्यवाद" };
console.log(hindi.language, hindi.hello);
Output
hi नमस्ते

Record#

Record<Keys, Value> is a built-in shorthand for the same idea:

TypeScript
const wordCounts: Record<string, number> = {};

for (const word of "the cat and the hat".split(" ")) {
  wordCounts[word] = (wordCounts[word] ?? 0) + 1;
}

console.log(wordCounts);
Output
{ the: 2, cat: 1, and: 1, hat: 1 }

Record<string, number> and { [key: string]: number } are equivalent; Record is shorter and more common.

Record with a union of keys#

When the keys come from a known set, use a literal union. Then every key is required, which catches missing entries:

TypeScript
type Plan = "free" | "pro" | "team";

const monthlyPrice: Record<Plan, number> = {
  free: 0,
  pro: 299,
  team: 999,
};

function priceFor(plan: Plan): number {
  return monthlyPrice[plan];
}

console.log(priceFor("pro"));
Output
299
TypeScript
type Plan = "free" | "pro" | "team";
const monthlyPrice: Record<Plan, number> = { free: 0, pro: 299 };
Compiler error
error TS2741: Property 'team' is missing in type '{ free: number; pro: number; }' but required in type 'Record<Plan, number>'.

This is a great way to build lookup tables that stay complete as the union grows. Partial<Record<Plan, number>> makes every key optional (Utility Types).

Reading unknown keys#

With a string index signature, TypeScript assumes every key exists, so reading a missing one gives a number type even though the value is undefined at runtime:

TypeScript
const prices: Record<string, number> = { tea: 20 };
const coffee = prices.coffee;          // type: number (but really undefined)
console.log(coffee, typeof coffee);
Output
undefined undefined

The compiler option noUncheckedIndexedAccess fixes this by making such reads number | undefined, so you must check them. Many teams enable it (tsconfig.json). Until then, treat dictionary reads as possibly missing and use ?? or in checks.

Record or Map?#

Record / plain objectMap
KeysStrings (and symbols, numbers as strings)Any type, including objects
JSONWorks directlyNeeds conversion
SizeObject.keys(obj).lengthmap.size
Frequent adds and deletesFine for small dataDesigned for it
Typed asRecord<string, V>Map<string, V>

For data that will be sent as JSON or used as a fixed lookup table, use Record. For caches, counters with non-string keys or heavy add/delete use, use Map.

Practice#

Practice exercise

Define type Subject = "maths" | "science" | "english" and type ReportCard = Record<Subject, number>. Write best(card: ReportCard): Subject that returns the subject with the highest mark, and test it.

Show solution
TypeScript
type Subject = "maths" | "science" | "english";
type ReportCard = Record<Subject, number>;

function best(card: ReportCard): Subject {
  const subjects = Object.keys(card) as Subject[];
  return subjects.reduce((top, s) => (card[s] > card[top] ? s : top));
}

console.log(best({ maths: 88, science: 94, english: 79 }));
Output
science

Object.keys returns string[] (objects can have extra keys at runtime), so the result is asserted to Subject[] with as. Assertions are covered in as, as const, satisfies and !.

Key takeaway#

Finished this lesson?