Skip to main content
interlace
Plugin: conventionsRules

consistent-existence-index-check

Enforce one form for checking whether an object has a property

Enforce one form for checking whether an object has a property — Object.hasOwn, in, or one of the hasOwnProperty spellings. This rule is part of eslint-plugin-conventions.

The name is about property existence, not array indexing. Despite index in the rule id, this rule never looks at indexOf or includes. It reads key in obj, obj.hasOwnProperty(key), Object.prototype.hasOwnProperty.call(obj, key) and Object.hasOwn(obj, key).

Quick Summary

AspectDetails
SeverityWarning (code quality)
Auto-Fix⚠️ One conversion only — see below
CategoryQuality
ESLint MCP✅ Optimized for ESLint MCP integration
Best ForConsistency, and keeping prototype lookups out by default

Rule Details

JavaScript has four ways to ask whether an object has a property, and they do not all answer the same question. This rule picks one and reports the others.

FormAnswers for an inherited keyLooks the method up on obj
Object.hasOwn(obj, key)nono
Object.prototype.hasOwnProperty.call(obj, key)nono
obj.hasOwnProperty(key)noyes
key in objyesno

Why This Matters

IssueImpactSolution
🛡️ Prototype chainin is true for inherited keys — the pollution directionDefault to Object.hasOwn
💥 Dispatchobj.hasOwnProperty throws on a null-prototype objectNever call it through obj
🔄 ConsistencyFour spellings of one questionStandardize on one

Examples

The default preference is Object.hasOwn.

❌ Incorrect

key in obj; // answers true for INHERITED keys
obj.hasOwnProperty(key); // looks the method up ON obj
Object.prototype.hasOwnProperty.call(obj, key); // the long way round

✅ Correct

Object.hasOwn(obj, key);

What is and is not autofixed

Only Object.prototype.hasOwnProperty.call(obj, key)Object.hasOwn(obj, key). Those two ask the same question through the same dispatch, so the rewrite is safe.

Everything else is reported without a fix, because rewriting it would change what the code does:

  • in walks the prototype chain and the own-property checks do not, so the two disagree on an inherited key.
  • obj.hasOwnProperty(key) looks the method up on obj: it throws on a null-prototype object and calls whatever a shadowing own property points at.
  • a surplus or sequence-expression argument would change the argument list the call is handed.

Those reports also carry a different message, because the ordinary one ("Use <preferred> instead of <current>") is an instruction to perform exactly the rewrite the rule just declined to make. Following it breaks working code: Object.hasOwn is declared hasOwn(o: object, v: PropertyKey): boolean and is not a type predicate, so rewriting 'on' in target loses the narrowing in performed and the following target.on(...) stops typechecking; and at runtime 'on' in emitter is true while Object.hasOwn(emitter, 'on') is false, because on lives on the prototype.

So a non-rewritable site reads:

Fix: Change this site by hand: "in" and "Object.hasOwn" disagree on an inherited key

naming what actually differs — an inherited key, method dispatch on the object, or the argument list — instead of ordering the rewrite.

In TypeScript, in also narrows

in is a type guard. Object.hasOwn is not, and TypeScript has no plan to make it one — it cannot, because hasOwn is an ordinary call whose return type says nothing about its arguments. So on a discriminated union the two forms are not interchangeable even when they agree at runtime:

type A = { kind: 'a'; reason: string };
type B = { kind: 'b' };

declare const x: A | B;

if ('reason' in x) x.reason; // narrows to A — compiles
if (Object.hasOwn(x, 'reason')) x.reason; // TS2339: Property 'reason' does not exist on type 'A | B'

Measured on one TypeScript codebase, every one of 48 reports of this rule sat on a discriminated union, and following each of them would have replaced working narrowing with a cast. A TypeScript project that uses in for narrowing wants preferred: 'in', and that is not the same as wanting a prototype-chain lookup.

The rule never rewrites across this boundary on its own — see the autofix section above — so the reports are a prompt, not a silent change.

Options

preferred: 'Object.hasOwn' | 'in' | 'hasOwnProperty' — default 'Object.hasOwn'.

Set 'in' when a prototype-chain lookup is what the code means, or when the project uses in to narrow unions in TypeScript. In plain JavaScript it is a choice worth making deliberately; it is not a good default there, which is why it is no longer one.

Configuration Examples

Basic Usage

{
  rules: {
    // Default: preferred is 'Object.hasOwn'
    'conventions/consistent-existence-index-check': 'warn',

    // Or state a different preference deliberately — a TypeScript project that
    // narrows unions with `in` wants this one
    // 'conventions/consistent-existence-index-check': ['warn', { preferred: 'in' }],
  }
}

Further Reading

Known False Negatives

The following patterns are not detected due to static analysis limitations:

Dynamic Variable References

Why: Static analysis cannot trace values stored in variables or passed through function parameters.

// ❌ NOT DETECTED - Value from variable
const value = externalSource();
processValue(value); // Variable origin not tracked

Mitigation: Implement runtime validation and review code manually. Consider using TypeScript branded types for validated inputs.

Imported Values

Why: When values come from imports, the rule cannot analyze their origin or construction.

// ❌ NOT DETECTED - Value from import
import { getValue } from './helpers';
processValue(getValue()); // Cross-file not tracked

Mitigation: Ensure imported values follow the same constraints. Use TypeScript for type safety.

Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.