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.
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#
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);4 35[item: string]: number means "any string key maps to a number". The name item is just for documentation. Values must still match:
type PriceList = { [item: string]: number };
const prices: PriceList = { tea: 20 };
prices.juice = "fifty";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:
type Translations = {
[key: string]: string;
language: string;
};
const hindi: Translations = { language: "hi", hello: "नमस्ते", thanks: "धन्यवाद" };
console.log(hindi.language, hindi.hello);hi नमस्तेRecord#
Record<Keys, Value> is a built-in shorthand for the same idea:
const wordCounts: Record<string, number> = {};
for (const word of "the cat and the hat".split(" ")) {
wordCounts[word] = (wordCounts[word] ?? 0) + 1;
}
console.log(wordCounts);{ 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:
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"));299type Plan = "free" | "pro" | "team";
const monthlyPrice: Record<Plan, number> = { free: 0, pro: 299 };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:
const prices: Record<string, number> = { tea: 20 };
const coffee = prices.coffee; // type: number (but really undefined)
console.log(coffee, typeof coffee);undefined undefinedThe 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?#
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
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 }));scienceObject.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?