JavaScript Dictionary: Object or Map?

Oct 14, 2026·20 min read

JavaScript code often needs a collection where each key identifies one value: a status code names a label, a user ID finds a record, or a word points to its running count. Both Object and Map can hold that relationship, but they do not treat keys the same way.

Use an Object for record-shaped data with known string keys, use a null-prototype Object for externally supplied string keys, use a Map for a dynamic keyed collection, and reserve WeakMap for metadata tied to eligible keys that should not be kept alive.

Does JavaScript Have a Dictionary?

A dictionary is a collection of unique keys associated with values. Writing an existing key again replaces its value rather than creating a second entry with the same key.

One key has one slotdraft→ Draftdraft→ In reviewdictionarydraftIn reviewstill one entry
Writing the same key replaces its value.

JavaScript has no built-in type named Dictionary. Instead, JavaScript programs commonly use either an Object or a Map for dictionary-like work.

An Object fits data shaped like a record. Its keys name fields such as title, status, and author, and those keys are strings or Symbols. Object literals also fit naturally into JSON:

const article = {
  title: "Array methods",
  status: "draft",
};

console.log(article.status);
draft

A Map fits a collection that grows and changes while the program runs. It accepts values of any type as keys, exposes its entry count through size, and is directly iterable:

const articlesById = new Map([
  [101, "Array methods"],
  [205, "Object properties"],
]);

console.log(articlesById.get(205));
Object properties

The immediate rule is small: choose Object for named fields and Map for dynamic entries. Objects and Map and Set develop each structure separately.

Two different shapes of dataObject recordtitlestatusauthornamed slotsMap collection101 → article205 → article+grows at runtime
Object describes a record; Map collects entries.

Using an Object as a Dictionary

Start with a status-label lookup table. Each property name is a status, and each property value is the text shown in the interface:

const statusLabels = {
  draft: "Draft",
  published: "Published",
};

const nextStatus = "pending";

statusLabels[nextStatus] = "Awaiting review";
statusLabels.draft = "Work in progress";

console.log(statusLabels.draft);
console.log(statusLabels["pending"]);

delete statusLabels.pending;

console.log(Object.hasOwn(statusLabels, "published"));
console.log(Object.hasOwn(statusLabels, "pending"));
console.log(Object.keys(statusLabels));
console.log(Object.values(statusLabels));
console.log(Object.entries(statusLabels));
Work in progress
Awaiting review
true
false
[ 'draft', 'published' ]
[ 'Work in progress', 'Published' ]
[ [ 'draft', 'Work in progress' ], [ 'published', 'Published' ] ]

Bracket notation accepts an expression, so statusLabels[nextStatus] uses the string stored in nextStatus. Dot notation uses the literal property name after the dot. You need brackets when the key comes from a variable or contains characters that do not form a JavaScript identifier.

Assignment creates a property or updates an existing one. delete removes it.

An own property belongs directly to the object rather than coming from its prototype chain. Object.hasOwn(statusLabels, key) checks for that exact kind of property, including one whose value is undefined.

The three enumeration methods return parallel views of the same own enumerable string-keyed properties:

  • Object.keys(object) returns the property names.
  • Object.values(object) returns their values.
  • Object.entries(object) returns [key, value] pairs.

Those arrays make it possible to count and iterate the dictionary:

const labels = {
  draft: "Work in progress",
  published: "Published",
};

console.log(Object.keys(labels).length);

for (const [status, label] of Object.entries(labels)) {
  console.log(`${status}: ${label}`);
}
2
draft: Work in progress
published: Published

Own property keys are ordered with array-index strings first in ascending numeric order, then other string keys in creation order, then Symbol keys in creation order. Object.keys(), Object.values(), and Object.entries() include only enumerable string keys; Object.getOwnPropertyNames() includes non-enumerable string keys too, Object.getOwnPropertySymbols() returns Symbol keys, and Reflect.ownKeys() includes both string and Symbol keys. These rules and different enumeration mechanisms make Object a poor substitute for Map’s direct insertion-order model. Use object properties because the data has named fields, not because insertion order happens to look right in one example. Object.keys, values, entries covers the enumeration APIs in detail.

Using Map as a Dictionary

Now build the same status-label lookup with Map. Its entries are added as [key, value] pairs or through set():

const statusLabels = new Map([
  ["draft", "Draft"],
  ["published", "Published"],
]);

statusLabels.set("pending", "Awaiting review");
statusLabels.set("draft", "Work in progress");

console.log(statusLabels.get("draft"));
console.log(statusLabels.has("published"));

statusLabels.delete("pending");

console.log(statusLabels.has("pending"));
console.log(statusLabels.size);

for (const [status, label] of statusLabels) {
  console.log(`${status}: ${label}`);
}

const temporaryLabels = new Map(statusLabels);
temporaryLabels.clear();

console.log(temporaryLabels.size);
Work in progress
true
false
2
draft: Work in progress
published: Published
0

set() creates or updates an entry. get() reads its value, has() checks membership, delete() removes one entry, and clear() removes every entry. The size property reports the current count without creating a separate keys array.

A Map also accepts numbers and objects as keys without converting them to strings:

const reviewer = { id: 7, name: "Maya" };
const labels = new Map();

labels.set(404, "Missing");
labels.set(reviewer, "Assigned reviewer");

console.log(labels.get(404));
console.log(labels.get(reviewer));
console.log(labels.has({ id: 7, name: "Maya" }));
Missing
Assigned reviewer
false

The last lookup uses a new object. It has the same fields as reviewer, but it is not the same object, so it does not match the stored key. Map preserves object identity.

Map remembers the object itselforiginal reviewer{ id: 7 }new look-alike{ id: 7 }Mapsame object→ Assignedmatchno match
Look-alike objects are different Map keys.

Map entries iterate in insertion order. You can loop over [key, value] pairs directly, or use map.keys(), map.values(), and map.entries() when one view is more useful.

Object vs. Map: Which Should You Choose?

The choice starts with the shape of the data, not a claim that one structure is always faster.

StructureChoose it forKeysIteration and sizeJSON and lifetime
ObjectRecords with known field namesString or Symbol property keysUse Object enumeration methods and count their resultsEnumerable own properties serialize directly
Null-prototype ObjectA string-keyed dictionary containing unconstrained external namesString or Symbol property keysUse Object enumeration methods and Object.hasOwn()Enumerable own properties serialize directly
MapA dynamic collection where key type or identity mattersAny JavaScript valueDirect iteration and sizeConvert entries explicitly before JSON serialization
WeakMapMetadata attached to object keysObjects or non-registered Symbols created with Symbol(); symbols returned by Symbol.for() are registered and cannot be WeakMap keysCannot be enumerated or countedEntries can disappear when their keys are collected

Use an Object for a customer record with fields such as name, email, and plan. The field names describe the data, code commonly reads them with dot notation, and JSON serialization is part of the job.

Use a Map for records indexed by numeric IDs, DOM nodes, configuration objects, or other keys whose type and identity must survive. It also fits counters, caches, and registries whose keys are discovered while the program runs.

Use Object.create(null) when the keys must be strings but arrive from an external source:

const translations = Object.create(null);
const externalKey = "constructor";

translations[externalKey] = "Constructor label";

console.log(Object.hasOwn(translations, "constructor"));
console.log(translations.constructor);
true
Constructor label

A null-prototype object inherits nothing from Object.prototype. That removes collisions with inherited names such as constructor and makes the object behave more like a bare string-keyed table.

What sits underneath the table?ordinary objectexternal keysObject.prototypeconstructor, toStringnull-prototypeobjectexternal keysonlyno parent layerinherited names can appearbare string-keyed table
A null-prototype object is a table without an inherited layer.

Use WeakMap for non-enumerable metadata associated with objects:

const validationState = new WeakMap();
const form = { name: "signup" };

validationState.set(form, { valid: false });

console.log(validationState.get(form).valid);
false

WeakMap is not a general replacement for Map because you cannot enumerate its entries or ask for their count. Its special job is attaching data without preventing an eligible key from being garbage-collected. If the metadata must also be private, keep the WeakMap itself in a private lexical or module scope.

Is this metadata tied to object keys?
├─ Yes: Must it be enumerated or counted?
│  ├─ No → WeakMap
│  └─ Yes → Map
└─ No: Are the keys unconstrained external strings?
   ├─ Yes → null-prototype Object
   └─ No: Does key type or identity matter?
      ├─ Yes → Map
      └─ No: Are these known named fields?
         ├─ Yes → Object
         └─ No → Map

These distinctions also matter when Object references and copying enter the code, because copying a record and copying a keyed collection are different operations.

The Dictionary Traps That Cause Real Bugs

Object property keys can only be strings or Symbols. A number used as an object key becomes a string, and an object used as a key is converted too:

const labels = {};
const firstUser = { id: 1 };
const secondUser = { id: 2 };

labels[1] = "numeric key";
labels[firstUser] = "first";
labels[secondUser] = "second";

console.log(labels["1"]);
console.log(labels[firstUser]);
console.log(Object.keys(labels));
numeric key
second
[ '1', '[object Object]' ]

The number 1 and string "1" address the same property. Both user objects also produce the same default property name, so the second assignment overwrites the first. Map keeps those keys distinct. Object to primitive conversion explains the conversion that produces the string.

Different keys can collapse in an Object1 number”1” stringone Object property”1”user { id: 1 }user { id: 2 }one Object property[object Object]the later value wins
Object key conversion can collapse distinct keys.

Inherited properties create another false match:

const labels = { draft: undefined };

console.log("toString" in labels);
console.log(Object.hasOwn(labels, "toString"));
console.log(labels.missing === undefined);
console.log(labels.draft === undefined);
console.log(Object.hasOwn(labels, "missing"));
console.log(Object.hasOwn(labels, "draft"));
true
false
true
true
false
true

in sees inherited properties, so "toString" in labels is true. Reading alone also fails to distinguish the absent missing property from the present draft property whose value is undefined.

Same read, different statemissingno propertydraftstores undefinedread → undefinedread → undefinedhasOwn → falsehasOwn → true
A read returns undefined in both cases; ownership does not.

Use Object.hasOwn(object, key) for an object dictionary and map.has(key) for a Map. Do not call dictionary.hasOwnProperty(key) directly: a null-prototype object has no such method, and an ordinary object can contain its own property named hasOwnProperty.

Unconstrained external keys add a prototype-related risk. For example, even a null-prototype source can change the prototype of an ordinary target when copied with Object.assign():

const source = Object.create(null);
source["__proto__"] = { isAdmin: true };

const unsafeCopy = Object.assign({}, source);
console.log(unsafeCopy.isAdmin);

const safeCopy = Object.assign(Object.create(null), source);
console.log(Object.hasOwn(safeCopy, "__proto__"));
console.log(safeCopy.isAdmin);
true
true
undefined

Null-prototype storage avoids inherited-name collisions and the legacy __proto__ setter on that object. It does not make later copies into ordinary objects safe or remove the need to validate untrusted names when application code gives them special meaning. Prefer Map when keys do not need to become object properties, or keep both the original dictionary and its copies as objects created with Object.create(null).

Map has its own quiet mistake:

const labels = new Map();

labels["draft"] = "Draft";

console.log(labels.has("draft"));
console.log(labels.get("draft"));
console.log(labels["draft"]);
console.log(labels.size);
false
undefined
Draft
0

labels["draft"] creates an ordinary property on the Map object. It does not create a Map entry. Use labels.set("draft", "Draft").

A Map has two different placeslabels[“draft”]= “Draft”labels.set(“draft”, “Draft”)Map objectordinary propertydraftMap entriesdraft → Draftsize counts only this entry store
Bracket assignment lands outside the Map entry store.

Performance does not settle the choice by itself. Map is required to provide average access time that is sublinear in its entry count, but that is not a promise that every operation is constant time. Workload, engine, key shape, entry count, and memory use all affect a real program. Pick the semantics first, then measure the code that matters.

Convert Arrays, Objects, and Maps

Entry arrays provide a common bridge between these structures. Each entry is a two-item [key, value] array:

const entries = [
  ["draft", "Draft"],
  ["published", "Published"],
];

const labelsObject = Object.fromEntries(entries);
const labelsMap = new Map(entries);
const mapFromObject = new Map(Object.entries(labelsObject));
const objectFromMap = Object.fromEntries(labelsMap);

console.log(labelsObject.draft);
console.log(labelsMap.get("published"));
console.log(mapFromObject.size);
console.log(objectFromMap);
Draft
Published
2
{ draft: 'Draft', published: 'Published' }

Object.fromEntries(entries) creates an object. new Map(entries) creates a Map. To cross between them, pass Object.entries(object) to Map or pass a Map to Object.fromEntries().

That final conversion can lose key identity. Object property keys cannot preserve arbitrary Map key types:

const keyedLabels = new Map([
  [1, "number key"],
  ["1", "string key"],
]);

const labelsObject = Object.fromEntries(keyedLabels);

console.log(labelsObject["1"]);
console.log(Object.keys(labelsObject).length);
string key
1

The number 1 became the property name "1", so the later string key replaced its value. Object keys also cannot preserve the identity of object keys held by a Map.

JSON needs the same explicit decision:

const labels = new Map([
  ["draft", "Draft"],
  ["published", "Published"],
]);

console.log(JSON.stringify(labels));
console.log(JSON.stringify(Object.fromEntries(labels)));
{}
{"draft":"Draft","published":"Published"}

Map entries are not enumerable own properties, so JSON.stringify(labels) produces {}. Convert the Map only when its keys can safely become object property keys, or define an application-specific representation such as an array of entries. JSON methods and toJSON covers the serialization rules behind the result.

What JSON.stringify can seeMapinternal entriesdraft → Draftpublished → …property scanJSON result{}convert entries beforeserializationentries stay behind this boundary
JSON.stringify cannot see Map entries directly.

Practical JavaScript Dictionary Patterns

A fixed status-label lookup has known string keys, so an Object matches its record-like shape:

const statusLabels = {
  draft: "Draft",
  review: "Ready for review",
  published: "Published",
};

console.log(statusLabels.review);
Ready for review

An index of records by numeric ID is a dynamic collection, so Map preserves the numeric keys:

const articles = [
  { id: 101, title: "Arrays" },
  { id: 205, title: "Objects" },
];

const articlesById = new Map(
  articles.map((article) => [article.id, article])
);

console.log(articlesById.get(205).title);
Objects

A counter also grows from values encountered at runtime:

const tags = ["javascript", "css", "javascript", "html", "javascript"];
const counts = new Map();

for (const tag of tags) {
  counts.set(tag, (counts.get(tag) ?? 0) + 1);
}

console.log(counts.get("javascript"));
console.log([...counts]);
3
[ [ 'javascript', 3 ], [ 'css', 1 ], [ 'html', 1 ] ]

The fixed lookup uses Object, while the index and counter use Map because their entries are built from runtime data. That selection rule scales beyond these examples, and JavaScript Fundamentals places it alongside the object, array, type, and function rules used throughout larger programs.

Frequently asked questions

Does JavaScript have a dictionary type?
JavaScript has no built-in type named Dictionary. Use an Object for record-shaped data with named string keys, or a Map for a dynamic collection whose keys may have any type.
Should I use an Object or Map as a JavaScript dictionary?
Use an Object when the keys are known field names and the data needs direct JSON serialization. Use a Map when keys are added dynamically, key type matters, or you need built-in size and iteration.
How do I check whether a JavaScript dictionary contains a key?
Use Object.hasOwn(dictionary, key) for an object and dictionary.has(key) for a Map. Reading the value is not enough because both structures can store undefined.
How do I convert an Object to a Map in JavaScript?
Pass Object.entries(object) to the Map constructor: new Map(Object.entries(object)). The resulting Map receives the object's own enumerable string-keyed entries.
Why does JSON.stringify return an empty object for a Map?
JSON.stringify visits enumerable own properties, while Map entries are not object properties. Convert the Map with Object.fromEntries(map) when its keys can safely become object property keys.