How can I properly create and use a JavaScript dictionary?

I’m trying to store key value pairs in JavaScript like a dictionary, but I’m confused about when to use plain objects vs Map, and how to avoid common pitfalls like key collisions or unexpected prototype properties. I need clear guidance and simple examples so I can decide the best approach for my project and refactor my existing code without breaking anything.

Short version

Use plain objects for simple string keys.
Use Map when you need non string keys or need reliable behavior.

  1. Plain object as a “dictionary”

Pros

  • Fast for simple lookups
  • Literal syntax is short

Basic usage:

const dict = {
foo: 1,
bar: 2,
};

dict.baz = 3;
console.log(dict[‘foo’]); // 1

To avoid prototype junk and key collisions, do this instead:

const dict = Object.create(null);

dict.foo = 1;
dict[‘proto’] = ‘safe’;
console.log(dict.proto); // undefined
console.log(dict[‘proto’]); // ‘safe’

Object.create(null) gives you a bare object with no prototype, so no inherited keys like toString, constructor, etc.

Safe checks:

if (‘foo’ in dict) {
// key exists
}

or

if (Object.hasOwn(dict, ‘foo’)) {
// no prototype check
}

Common gotchas with plain objects

  • Keys get forced to strings
  • Order of keys is not guaranteed for numbers
  • hasOwnProperty can be shadowed:

const dict = Object.create(null);
// hasOwnProperty does not exist

That is why Object.hasOwn is better.

  1. Map as a dictionary

Use Map when:

  • Keys are not only strings (objects, numbers, functions)
  • You need stable insertion order
  • You want clear API for size, iterate, delete

Example:

const m = new Map();

m.set(‘foo’, 1);
m.set(123, ‘num’);
m.set({ id: 1 }, ‘obj’);

console.log(m.get(‘foo’)); // 1
console.log(m.get(123)); // ‘num’
console.log(m.size); // 3

Check, delete, iterate:

if (m.has(‘foo’)) {
m.delete(‘foo’);
}

for (const [key, value] of m) {
console.log(key, value);
}

No prototype problems. No collisions with proto.

  1. When to pick what

Use plain object if:

  • Keys are only strings
  • You do not need to frequently add and delete
  • You want JSON friendly structure

Use Object.create(null) when:

  • You care about security or odd keys like ‘proto
  • You want a “pure” dictionary

Use Map if:

  • You need non string keys
  • You do a lot of adds/deletes/lookups
  • You care about insertion order
  • You want a clean API
  1. Extra patterns

Simple dictionary helper with plain object:

function makeDict() {
return Object.create(null);
}

const usersById = makeDict();
usersById[‘42’] = { id: 42, name: ‘Alice’ };

Map alternative:

const usersById = new Map();
usersById.set(42, { id: 42, name: ‘Alice’ });

Some perf tests show Map tends to be faster for large dynamic key sets, especially with many deletes. For small stuff you will not notice.

If you feel confused, default to:

  • Object.create(null) for JSON like data
  • Map for anything more “data structure” style

And yeah, avoid this pattern:

const dict = {};
dict[‘proto’] = ‘oops’;

This bites a lot of people.

Couple of extra angles to add on top of what @mikeappsreviewer already covered.

1. Don’t overthink it for “config-ish” data

If your “dictionary” is basically static config or JSON-shaped data:

const options = {
  apiUrl: '/v1',
  timeout: 5000,
  mode: 'dark',
};

Just use a plain object literal. No Map, no Object.create(null). In real apps this covers a huge percentage of use cases. People jump to Map way too fast because it looks “CS-ish”.

2. Collisions & prototypes: practical view

Yes, Object.create(null) avoids __proto__ and other inherited stuff, but there’s a tradeoff:

  • No prototype means no toString, no hasOwnProperty, etc.
  • Debugging in devtools can be slightly more annoying because it looks a bit “alien”.

In most everyday code, prototype collisions happen only if:

  1. You accept arbitrary keys from untrusted input (user, network, etc), and
  2. You write them directly as keys on a normal {} object.

If both are true, then worry about Object.create(null) or just use Map. Otherwise, {} is fine and simpler.

3. When Map is secretly the right tool

Places where Map is actually worth it:

  • You have a cache keyed by objects:

    const cache = new Map();
    
    function getUserData(user) {
      if (cache.has(user)) return cache.get(user);
      const data = fetchStuff(user.id);
      cache.set(user, data);
      return data;
    }
    

    You simply cannot model “object identity” like that with {}.

  • You need lots of dynamic adds/removes in a hot path (routing tables, memoization caches, etc). Map is usually less weird perf-wise when the key set changes a lot.

  • You truly care about insertion order for iteration behavior and want that to be clear to the next dev reading the code.

4. String vs non-string keys: subtle gotcha

A point that bites people:

const o = {};
o[1] = 'a';
o['1'] = 'b';
console.log(o[1]); // 'b'

With a Map:

const m = new Map();
m.set(1, 'a');
m.set('1', 'b');
console.log(m.get(1));   // 'a'
console.log(m.get('1')); // 'b'

So if your numeric keys really matter as numbers, not as “labels”, Map is less surprising.

5. Checking existence: pick one style and stick with it

Mixin of options with objects:

const dict = Object.create(null);
// or const dict = {};

dict.key = 123;

// includes prototype chain
if ('key' in dict) {}

// own only, without trusting dict.hasOwnProperty
if (Object.hasOwn(dict, 'key')) {}

I mildly disagree with always pushing Object.create(null) for “safety.” In many codebases, “safety” mostly means “other devs instantly understand it.” A plain object with Object.hasOwn is usually good enough and easier to grok.

For Map:

if (m.has(key)) {
  // done
}

Super explicit, which is one of the nicer parts of the API.

6. Mutation patterns & readability

Objects encourage this style:

const dict = {};
dict.foo = 1;
dict.bar = 2;
delete dict.foo;

Map reads more like a standard data structure:

const dict = new Map();
dict.set('foo', 1);
dict.set('bar', 2);
dict.delete('foo');

Personally, I use:

  • Object literals for “data”
  • Map for “data structure”

Looks similar on the surface, but it keeps mental overhead lower: config vs algorithm.

7. What to actually do in your situation

Given your confusion and the typical web-app use case, a sane default strategy:

  1. Start with a plain object literal:

    const dict = {};
    
  2. If you start:

    • using objects as keys, or
    • needing reliable insertion order for iteration, or
    • doing tons of add/remove operations

    then refactor to:

    const dict = new Map();
    
  3. If you accept arbitrary untrusted keys and must write them as dictionary keys, use:

    const dict = Object.create(null);
    

    and always check using Object.hasOwn(dict, key).

Everything else is just nuance and perf micro-optimizations that barely matter until your profiler tells you it matters.

And yeah, if your first instinct is “this looks like a JSON object,” use {} and move on.