JavaScript Basics: Check Own Properties and Build Safe Dictionaries
Object lookup normally follows a prototype chain. That is useful for methods, but surprising when an object represents a dictionary of user-supplied labels. Object.hasOwn() distinguishes a stored key from an inherited property, and a null-prototype object avoids inherited dictionary entries.
These examples were tested on Linux with Node.js v24.19.0. They use built-in APIs only, require no network access, and include assertions that check the results. See MDN’s Object.hasOwn reference and Object.create reference.
Prepare a scratch directory
You need Bash, a text editor, Node.js 24 on your PATH, and the mktemp, rm, and rmdir utilities. Keep using the same terminal. Save each named source file as UTF-8; .mjs selects ES modules. Each example runs independently.
node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1The tested runtime prints:
v24.19.01. Distinguish an absent setting from an undefined value
Save this as check-own-settings.mjs:
import assert from 'node:assert/strict';
const settings = Object.create({ theme: 'dark' });
settings.timeout = undefined;
assert.equal('theme' in settings, true);
assert.equal(Object.hasOwn(settings, 'theme'), false);
assert.equal(Object.hasOwn(settings, 'timeout'), true);
assert.equal(Object.hasOwn(settings, 'missing'), false);
console.log(`Inherited theme: ${settings.theme}`);
console.log(`Own theme: ${Object.hasOwn(settings, 'theme')}`);
console.log(`Own timeout: ${Object.hasOwn(settings, 'timeout')}`);Run it:
node check-own-settings.mjsOutput:
Inherited theme: dark
Own theme: false
Own timeout: truein includes inherited properties, while Object.hasOwn() checks the object itself. Comparing a value with undefined cannot tell these cases apart. Own-property checks answer presence questions; they do not validate the value’s type or make reading a getter harmless.
2. Count labels without inherited key collisions
Save this as count-arbitrary-labels.mjs:
import assert from 'node:assert/strict';
const counts = Object.create(null);
for (const label of ['toString', '__proto__', 'toString', 'constructor']) {
counts[label] = (Object.hasOwn(counts, label) ? counts[label] : 0) + 1;
}
assert.equal(Object.getPrototypeOf(counts), null);
assert.equal(counts.toString, 2);
assert.equal(counts.__proto__, 1);
assert.equal(counts.constructor, 1);
console.log(JSON.stringify(counts));Run it:
node count-arbitrary-labels.mjsOutput:
{"toString":2,"__proto__":1,"constructor":1}A null-prototype object has no inherited toString method or legacy __proto__ setter. These names become ordinary data keys. It also lacks hasOwnProperty, so use the static check. Map is another good dictionary choice, especially when keys are not strings or symbols.
3. Copy only allowed own keys
Save this as copy-allowed-settings.mjs:
import assert from 'node:assert/strict';
const input = JSON.parse('{"theme":"dark","__proto__":{"admin":true},"hasOwnProperty":false}');
const cleaned = Object.create(null);
for (const key of ['theme', 'language']) {
if (Object.hasOwn(input, key)) cleaned[key] = input[key];
}
assert.equal(Object.hasOwn(input, '__proto__'), true);
assert.equal(Object.hasOwn(input, 'hasOwnProperty'), true);
assert.deepEqual(Object.keys(cleaned), ['theme']);
assert.equal(Object.getPrototypeOf(cleaned), null);
assert.equal(Object.hasOwn(cleaned, '__proto__'), false);
console.log(JSON.stringify(cleaned));
console.log('Unexpected keys excluded');Run it:
node copy-allowed-settings.mjsOutput:
{"theme":"dark"}
Unexpected keys excludedThe static check still works when input shadows hasOwnProperty. A fixed allowlist controls what this copy accepts. JSON parsing does not itself set the prototype from a __proto__ key, but careless later copying into ordinary objects can be dangerous. Validate allowed values and nested structures too; this shallow example is not a general-purpose security sanitizer.
Clean up
In the same terminal, remove only the named example files, return to your original directory, and remove the empty scratch directory:
rm -- check-own-settings.mjs count-arbitrary-labels.mjs copy-allowed-settings.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirAcknowledgements
This post was created with assistance from AI.