TypeScript has no separate dictionary or hashmap class. A dictionary is either a plain object typed with an index signature, { [key: string]: number }, the same type written Record<string, number>, or a Map<string, number>. All three store values by key; they differ in key types, how missing keys are typed, and how they serialize.
For string keys and JSON-shaped data, an object with Record<string, T> is the usual choice. For non-string keys, or entries added and removed all the time, use a Map.
Index Signatures
An index signature, [key: KeyType]: ValueType, says "any key of this type maps to a value of this type". The key name (key, name, userId) is only documentation. Key types can be string, number, symbol, template literal patterns, or unions of these.
Number keys are converted to strings by JavaScript, so a { [id: number]: string } object still has string keys at runtime: Object.keys({ 1: "one" }) is [ '1' ]. The number index signature only restricts how you may index it in TypeScript.
Record<K, V>
Record<string, V> is shorthand for { [key: string]: V }. With a union of literal keys instead of string, it becomes a fixed dictionary that must contain every key:
Leaving staging out of urls is compile error TS2741 (Property 'staging' is missing...), which makes Record with union keys a checked lookup table. Partial makes each value number | undefined.
Map as a Hash Map
A Map accepts keys of any type, keeps insertion order, has a size, and types missing keys honestly: get returns V | undefined.
Checking if a Key Exists
Several checks exist, and they do not all mean the same thing:
| Check | Works on | Watch out for |
|---|---|---|
Object.hasOwn(obj, key) | objects | ES2022; use Object.prototype.hasOwnProperty.call(obj, key) on older targets |
key in obj | objects | Also true for inherited keys like toString and constructor |
obj[key] !== undefined | objects | Cannot tell a missing key from one stored as undefined |
if (obj[key]) | objects | Also false for 0, "" and false values |
map.has(key) | Map | Does not narrow a later map.get(key) |
map.get(key) !== undefined | Map | Same undefined caveat as objects |
The inherited-key problem is why user-provided keys like "constructor" or "__proto__" make plain objects risky as dictionaries. A Map has no such keys.
The Missing-Key Type Problem
On an index signature or Record<string, T>, reading any key has type T, even a key that does not exist. The compiler lets you call methods on a value that is undefined at runtime:
The compiler option noUncheckedIndexedAccess fixes this: with it, colors["grass"] has type string | undefined and the toUpperCase call is a compile error until you check it. It is not part of strict, so it must be turned on separately in tsconfig.json; see strict mode for the other flags it sits beside. A Map has no such gap, since get always includes undefined.
Adding, Removing and Iterating
delete works on index-signature properties. On a required named property of an object type it is compile error TS2790, The operand of a 'delete' operator must be optional.
Which One to Use
| Need | Use |
|---|---|
| String keys, JSON in or out | Record<string, T> |
| A fixed, known set of keys, all required | Record<"a" | "b", T> |
| Named properties plus arbitrary extra keys | an object type with an index signature |
| Keys that are objects, numbers you keep as numbers, or any non-string | Map<K, V> |
| Frequent adds and deletes, or a size you read often | Map<K, V> |
| Keys that come from users | Map<K, V> (no inherited keys) |
Frequently Asked Questions
How do I create a dictionary in TypeScript?
Type a plain object with an index signature, const ages: { [name: string]: number } = {}, or the equivalent Record<string, number>. Then add entries with ages["ada"] = 36. For keys that are not strings, or a collection with frequent adds and deletes, use new Map<string, number>().
Does TypeScript have a HashMap?
Not under that name. JavaScript's built-in Map is a hash map: Map<K, V> stores key-value pairs with fast lookup by key, keeps insertion order, and accepts any key type. A plain object typed as Record<string, V> is the other common choice for string keys.
How do I check if a key exists in a TypeScript dictionary?
For an object dictionary use Object.hasOwn(dict, key) or key in dict (which also sees inherited properties such as toString), or read the value and compare it with undefined. For a Map, use map.has(key), or check the result of map.get(key) directly, since has does not narrow a later get.
What is the difference between an index signature and Record?
{ [key: string]: T } and Record<string, T> describe the same type. Record is shorter and can also take a union of specific keys, Record<"a" | "b", T>, which requires every key. An index signature can be combined with named properties in one object type, and its key can carry a name that documents it, as in { [userId: string]: User }.
Why does reading a missing dictionary key not give an error?
By default, dict[key] on an index signature or Record<string, T> has type T, even though the value is undefined for a missing key at runtime. Enable noUncheckedIndexedAccess in tsconfig.json and the type becomes T | undefined, forcing a check. strict does not include this option.