How to check if an object really has a key — without prototype gotchas, null-prototype crashes, or security footguns. With modern Object.hasOwn
, TypeScript patterns, and real-world examples.

Object.hasOwn
, TypeScript patterns, and real-world examples.Introduction
You need to know whether an object has a property.
Most codebases have all three of these floating around:
key in obj
obj.hasOwnProperty(key)
Object.prototype.hasOwnProperty.call(obj, key)
…and now there’s Object.hasOwn(obj, key)
, which (spoiler) is the clean, modern answer in most cases.
But the details matter. Prototype chain checks vs. own checks, shadowed methods, Object.create(null)
, symbol keys, sparse arrays, performance, and prototype pollution can turn a simple “has key?” into a subtle bug.
This post gives you a practical, senior-dev guide to hasOwnProperty
(and friends): when to use each, how they differ, and how to avoid all the common traps—plus TypeScript-friendly helpers you can paste into your project.
1) The Four Ways to Ask “Does this property exist?”
1.1) in
operator — own or inherited
"toString" in {} // true (inherited from Object.prototype)
"length" in [] // true (own)
- Looks through the prototype chain.
- Works with string or symbol keys.
- Useful when inherited defaults count, but often too broad.
1.2) obj.hasOwnProperty(key)
— own only, but fragile
const o = { x: 1 };
o.hasOwnProperty("x"); // true
- Checks own properties only (no prototypes).
- Fragile when:
- The object redefines
hasOwnProperty
. - The object has no prototype (e.g.,
Object.create(null)
).
1.3) Object.prototype.hasOwnProperty.call(obj, key)
— robust classic
Object.prototype.hasOwnProperty.call(o, "x");
- Works even if
obj
shadowshasOwnProperty
or has no prototype. - Verbose, but bulletproof on older environments.
1.4) Object.hasOwn(obj, key)
— modern, clean, safe
Object.hasOwn(o, "x"); // ✅ preferred in modern JS
- ES2022 added a built-in static method.
- Checks own properties; safe for null-prototype objects; not fooled by shadows.
- Accepts string or symbol keys.
Rule of thumb: Use
Object.hasOwn(obj, key)
in new code. Fall back toObject.prototype.hasOwnProperty.call(obj, key)
if you must support very old engines.
2) “Own” vs “Inherited”: Why You Got Burned Last Time
Own property = defined directly on the object.
Inherited = comes from the prototype.
const base = { a: 1 };
const obj = Object.create(base);
obj.b = 2;
"a" in obj // true (inherited)
Object.hasOwn(obj, "a") // false (not own)
Object.hasOwn(obj, "b") // true (own)
If you want to know whether this object was assigned key
, use own checks. If you’re okay with defaults inherited from prototypes (e.g., methods), in
is fine.
3) Why obj.hasOwnProperty
Is Dangerous (and How to Fix It)
A) Shadowed method
const bad = { hasOwnProperty: () => false, x: 1 };
bad.hasOwnProperty("x"); // false 😬
Object.hasOwn(bad, "x"); // true ✅
B) Null-prototype objects
const dict = Object.create(null);
dict.x = 1;
dict.hasOwnProperty; // undefined ❌
Object.hasOwn(dict, "x"); // true ✅
C) Safer classic call
Object.prototype.hasOwnProperty.call(dict, "x"); // true ✅
4) Arrays, Sparse Arrays, and hasOwn
Array indexes are properties too.
const arr = [1, , 3]; // hole at index 1
Object.hasOwn(arr, 0); // true
Object.hasOwn(arr, 1); // false (hole)
arr[1] === undefined; // true, but not "own"
Takeaway: If you need to know whether an index is actually present (not just equals undefined
), use own checks. This matters for sparse arrays (holes), where in
and hasOwn
behave differently from reading the value.
5) Strings, Numbers, Symbols (and Coercion)
- Property keys are strings or symbols under the hood.
- Numbers get coerced:
Object.hasOwn(obj, 1)
→ same as key"1"
. - Both
in
andObject.hasOwn
accept symbol keys:
const S = Symbol("secret");
const o = { [S]: 42 };
Object.hasOwn(o, S); // true
6) hasOwn
vs propertyIsEnumerable
Different questions:
Object.hasOwn(obj, key)
→ Is this an own property?Object.prototype.propertyIsEnumerable.call(obj, key)
→ Is it own and enumerable?
const o = {};
Object.defineProperty(o, "hidden", { value: 1, enumerable: false });
Object.hasOwn(o, "hidden"); // true
Object.prototype.propertyIsEnumerable.call(o, "hidden"); // false
Use propertyIsEnumerable
for “will it show up in Object.keys
/ for...in
/ spread / JSON?” checks.
7) Real-World Patterns (Copy-Paste Ready)
A) Safe dictionary object (null-prototype)
const dict = Object.create(null); // no accidental inheritance
dict.one = 1;
if (Object.hasOwn(dict, "one")) {
// ...
}
B) Merge with confidence (but block pollution!)
const BLOCKED = new Set(["__proto__", "prototype", "constructor"]);
function safeAssign(target, source) {
for (const [k, v] of Object.entries(source)) { // entries => own+enumerable
if (BLOCKED.has(k)) continue; // block prototype pollution
target[k] = v; // no need for hasOwn here
}
return target;
}
Object.entries
only returns own, enumerable, string keys. A separatehasOwn
check here is redundant.
C) Optional property vs. missing property
const config = { debug: undefined };
if ("debug" in config) {
// property exists (maybe undefined)
}
if (Object.hasOwn(config, "debug")) {
// it's an own key (even if its value is undefined)
}
Use in
or hasOwn
when you must distinguish missing from present but undefined.
D) Narrowing in control flow
type Settings = { theme?: "light" | "dark" };
function getTheme(s: Settings) {
if (Object.hasOwn(s, "theme")) {
const t = s.theme; // TypeScript: t is "light" | "dark" | undefined (see §10)
return t ?? "light";
}
return "light";
}
E) Feature detection without runtime errors
if (Object.hasOwn(window, "localStorage")) {
// Safe to use localStorage (still consider try/catch for quota)
}
8) Performance: What’s Actually Fast Enough?
- Micro-differences rarely matter; choose correctness and clarity.
Object.hasOwn
andhasOwnProperty.call
are both fast and safe.- Prefer
Object.keys/entries/values
for bulk iteration—they already restrict to own, enumerable properties and pair nicely with array methods. - If you’re checking in a hot loop, store references and avoid work you don’t need (e.g., don’t call
hasOwn
immediately afterObject.entries
—it’s redundant).
9) When to Use Map
Instead of Objects
If you’re implementing a true dictionary:
- Arbitrary key types (including objects/functions)
- Intentional insertion order for iteration
- Frequent adds/removes/lookups
Use Map
. It has a clear API (map.has(key)
, map.get(key)
) that avoids all the object-prototype weirdness.
const m = new Map();
m.set({ id: 1 }, "value");
m.has({ id: 1 }); // false (different reference)
Objects are great as records (fixed fields), configs, and JSON payloads. Maps are great as collections.
10) TypeScript: Clean Narrowing Patterns
TypeScript can’t always narrow from Object.hasOwn
by itself to a specific key type, but you can write helpers that improve ergonomics.
A) Generic “key exists” predicate
type Key = string | number | symbol;
export function hasOwn<T extends object, K extends Key>(
obj: T,
key: K
): key is K & keyof T {
return Object.hasOwn(obj, key);
}
// usage
declare const u: { id?: number; name?: string };
if (hasOwn(u, "name")) {
// key is "name" here; u.name is string | undefined
// (the presence check doesn't eliminate undefined because the value might be undefined)
}
Presence checks don’t prove non-undefined values; combine with
!= null
if needed.
B) Presence + defined guard
export function hasDefined<T extends object, K extends keyof T>(
obj: T,
key: K
): obj is T & Required<Pick<T, K>> {
return Object.hasOwn(obj, key) && (obj as any)[key] !== undefined;
}
// usage
type User = { id?: number; name?: string };
declare const user: User;
if (hasDefined(user, "id")) {
// user.id is now number
}
C) Discriminated unions with in
type Payload = { ok: true; data: string } | { ok: false; error: string };
function handle(p: Payload) {
if ("data" in p) {
// p is the success shape
} else {
// p is the error shape
}
}
Use in
when the presence of a key selects a union member (even if that key is on the prototype—usually you model these as own keys).
11) Security: Prototype Pollution Isn’t Solved by hasOwn
Prototype pollution attacks work by writing special keys like __proto__
or constructor
into plain objects, mutating Object.prototype
.
Object.hasOwn(obj, "__proto__")
returns true for an own key named"__proto__"
.- The danger is in assigning to those keys, not in checking for them.
Mitigation pattern (when merging/unmarshalling untrusted data):
const BLOCKED = new Set(["__proto__", "prototype", "constructor"]);
function safeMerge(target, source) {
for (const [k, v] of Object.entries(source)) {
if (BLOCKED.has(k)) continue;
target[k] = v;
}
return target;
}
Even better: validate payloads with Zod/Yup/Valibot and avoid raw merging altogether.
12) Common Pitfalls (and the Fix)
- Using
obj.hasOwnProperty
on null-prototype objects
- ✅ Use
Object.hasOwn(obj, key)
orObject.prototype.hasOwnProperty.call(obj, key)
.
2. Confusing “missing” with undefined
obj.x === undefined
does not tell you ifx
exists; it might exist and be set toundefined
.- ✅ Use
Object.hasOwn(obj, "x")
or"x" in obj
.
3. Calling hasOwn
after Object.entries
/keys
- Redundant; those APIs already return only own, enumerable string keys.
4. Forgetting symbols
Object.keys
/entries
ignore symbols.- ✅ Use
Object.getOwnPropertySymbols(obj)
for symbol keys, or check withObject.hasOwn(obj, sym)
directly.
5. Relying on for...in
without a guard
- Brings in prototype props.
- ✅ Use
Object.hasOwn(obj, key)
inside the loop, or preferObject.keys/entries
.
6. Prototype pollution through naive merges
- ✅ Block dangerous keys or validate input. (See §11)
13) Practical Recipes You’ll Reuse
A) Polyfill-friendly helper (avoid global patching)
export const hasOwn = (obj, key) =>
Object.hasOwn ? Object.hasOwn(obj, key)
: Object.prototype.hasOwnProperty.call(obj, key);
B) Exhaustive key guard (string + symbol)
function ownKeys(obj) {
return [
...Object.getOwnPropertyNames(obj), // string keys
...Object.getOwnPropertySymbols(obj) // symbol keys
];
}
C) Strict config reader (presence + defined value)
function readRequired<T extends object, K extends keyof T>(obj: T, key: K): T[K] {
if (!Object.hasOwn(obj, key)) throw new Error(`Missing key: ${String(key)}`);
const v = obj[key];
if (v === undefined || v === null) throw new Error(`Key ${String(key)} is nullish`);
return v;
}
D) Safe lookup in sparse arrays
function getAt(arr, i, fallback) {
return Object.hasOwn(arr, i) ? arr[i] : fallback;
}
getAt([1, , 3], 1, "missing"); // "missing"
14) Quick Reference Table

15) FAQ (Stuff Teammates Ask)
Q: Why does key in obj
return true for "toString"
?
Because it walks the prototype chain and toString
lives on Object.prototype
.
Q: Why didn’t my non-enumerable property show up in Object.keys
?Object.keys
returns own + enumerable string keys only. The prop exists; it’s just hidden from enumeration.
Q: Do I need hasOwn
inside a for...of Object.entries(obj)
?
No. entries
is already own + enumerable.
Q: Should I just always use Map
?
Use Objects as records (fixed fields, JSON) and Maps as dictionaries (arbitrary keys, heavy iteration). Different tools.
Conclusion
Checking whether a property exists sounds trivial — but correctness hinges on what you mean by “exists.”
- Need own only and a modern, safe API? →
Object.hasOwn(obj, key)
. - Supporting older environments? →
Object.prototype.hasOwnProperty.call(obj, key)
. - Need to know if a property comes from anywhere in the chain? →
key in obj
. - Care about enumerability (will it show up in
keys
/JSON)? →propertyIsEnumerable
.
Pro Tip: If you ever write obj.hasOwnProperty(...)
, pause and replace it with Object.hasOwn(obj, ...)
(or the .call
classic). Your future self—and your bug tracker—will thank you.
Call to Action (CTA)
What’s the nastiest “hasOwnProperty” bug you’ve seen — shadowed method, null-prototype crash, or pollution issue?
Drop your story (and fix) in the comments. If this helped, bookmark it and share it with a teammate who still writes obj.hasOwnProperty()
.
Leave a Reply