Skip to content

feat(checker): add Object.hasOwn() type narrowing#63610

Open
daishuge wants to merge 1 commit into
microsoft:mainfrom
daishuge:feat/object-hasown-narrowing
Open

feat(checker): add Object.hasOwn() type narrowing#63610
daishuge wants to merge 1 commit into
microsoft:mainfrom
daishuge:feat/object-hasown-narrowing

Conversation

@daishuge

@daishuge daishuge commented Jul 5, 2026

Copy link
Copy Markdown

Fixes #44253

Problem

Object.hasOwn(obj, key) is the modern replacement for obj.hasOwnProperty(key) and the in operator for own-property checks, but TypeScript does not narrow the type of obj after the call:

type Shape = { kind: "circle"; radius: number } | { kind: "square"; side: number };

function f(s: Shape) {
    if (Object.hasOwn(s, "radius")) {
        s.radius; // Error: Property 'radius' does not exist on type 'Shape'
    }

    // Workaround: use "in" operator instead
    if ("radius" in s) {
        s.radius; // OK — "in" narrows, but Object.hasOwn doesn't
    }
}

Approach

30 lines added to narrowTypeByCallExpression() in src/compiler/checker.ts. The implementation:

  1. Detects Object.hasOwn(obj, key) calls by checking that the receiver is the global ObjectConstructor
  2. Delegates to the existing narrowTypeByInKeyword — same narrowing semantics as the in operator, zero new narrowing logic
  3. Same-branch narrowing only — the else branch does not narrow, per @RyanCavanaugh and @DanielRosenwasser's discussion in Support for Object.hasOwn (lib.d.ts and narrowing) #44253 about else-branch semantics (the in operator already narrows in the else branch, but replicating that for Object.hasOwn was not part of this scope)

Before / After

// Discriminated unions
type Shape = { kind: "circle"; radius: number } | { kind: "square"; side: number };
declare const s: Shape;
if (Object.hasOwn(s, "radius")) {
    s;        // Before: Shape          | After: { kind: "circle"; radius: number }
    s.radius; // Before: error          | After: number
}

// Unknown property on object
declare const x: object;
if (Object.hasOwn(x, "foo")) {
    x; // Before: object | After: object & Record<"foo", unknown>
}

// Compounding checks
declare const u: { a: string } | { b: number } | { a: string; b: number };
if (Object.hasOwn(u, "a")) {
    u; // { a: string } | { a: string; b: number }
    if (Object.hasOwn(u, "b")) {
        u; // { a: string; b: number }
    }
}

Edge cases handled

  • Aliased Object reference: const Obj = Object; Obj.hasOwn(...) works because we check the type of the receiver, not the identifier name
  • Dynamic keys: Object.hasOwn(obj, dynamicVar) with a non-literal key type does not narrow (same as in)
  • exactOptionalPropertyTypes: correctly narrows optional property presence via containsMissingType + Record<key, unknown> intersection
  • Negated checks: if (!Object.hasOwn(obj, key)) does not narrow in the truthy branch (only same-branch narrowing)
  • Else of negation: if (!Object.hasOwn(o, "b")) {} else { o; } — the else branch sees assumeTrue=true via prefix unary inversion, so narrowing applies correctly

Scope

  • src/compiler/checker.ts: 30 lines in narrowTypeByCallExpression()
  • Test file: tests/cases/compiler/objectHasOwnNarrowing.ts with baseline
  • No new narrowing logic — reuses narrowTypeByInKeyword directly

Adds type narrowing for Object.hasOwn(obj, key) calls, reusing the
existing narrowTypeByInKeyword logic since the narrowing semantics
are identical to the in operator.
@typescript-automation typescript-automation Bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Jul 5, 2026
@daishuge

daishuge commented Jul 5, 2026

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@daishuge daishuge marked this pull request as ready for review July 5, 2026 09:11
Copilot AI review requested due to automatic review settings July 5, 2026 09:11
@typescript-automation

Copy link
Copy Markdown

The TypeScript team hasn't accepted the linked issue #44253. If you can get it accepted, this PR will have a better chance of being reviewed.

1 similar comment
@typescript-automation

Copy link
Copy Markdown

The TypeScript team hasn't accepted the linked issue #44253. If you can get it accepted, this PR will have a better chance of being reviewed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds control-flow type narrowing support for Object.hasOwn(obj, key) in the TypeScript checker, aligning the true-branch behavior with existing "key" in obj narrowing and validating via new compiler tests (including exactOptionalPropertyTypes behavior).

Changes:

  • Implement narrowing in narrowTypeByCallExpression() for Object.hasOwn(obj, key) by reusing narrowTypeByInKeyword for the object-operand case.
  • Add compiler test coverage for union narrowing, dynamic keys (no narrowing), negation/branch behavior, aliasing Object, and compounding checks.
  • Add exactOptionalPropertyTypes coverage and update reference baselines.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/compiler/checker.ts Adds new Object.hasOwn-based narrowing logic in control-flow analysis.
tests/cases/compiler/objectHasOwnNarrowing.ts New compiler test covering core narrowing scenarios and branch behavior.
tests/cases/compiler/objectHasOwnNarrowingExactOptional.ts New compiler test covering exactOptionalPropertyTypes interaction.
tests/baselines/reference/objectHasOwnNarrowing.types Reference baseline for the new narrowing test.
tests/baselines/reference/objectHasOwnNarrowing.symbols Symbols baseline for the new narrowing test.
tests/baselines/reference/objectHasOwnNarrowing.js JS baseline for the new narrowing test.
tests/baselines/reference/objectHasOwnNarrowingExactOptional.types Reference baseline for the exact-optional test.
tests/baselines/reference/objectHasOwnNarrowingExactOptional.symbols Symbols baseline for the exact-optional test.
tests/baselines/reference/objectHasOwnNarrowingExactOptional.js JS baseline for the exact-optional test.

Comment thread src/compiler/checker.ts
Comment on lines +30156 to +30158
if (isTypeUsableAsPropertyName(keyType) && getAccessedPropertyName(reference) === getPropertyNameFromType(keyType)) {
return getTypeWithFacts(type, assumeTrue ? TypeFacts.NEUndefined : TypeFacts.EQUndefined);
}
// No narrowing in else branch (same-branch only, per maintainer guidance)
declare const maybe: { x?: number };
if (Object.hasOwn(maybe, "x")) {
maybe; // narrowed to { x?: number } & Record<"x", unknown>
o4; // not narrowed (we only narrow in true branch)
}

// True branch after negation doesn't narrow either (the else of the if(!...))
@MartinJohns

Copy link
Copy Markdown
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for Object.hasOwn (lib.d.ts and narrowing)

3 participants