feat(checker): add Object.hasOwn() type narrowing#63610
Conversation
Adds type narrowing for Object.hasOwn(obj, key) calls, reusing the existing narrowTypeByInKeyword logic since the narrowing semantics are identical to the in operator.
|
@microsoft-github-policy-service agree |
|
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
|
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. |
There was a problem hiding this comment.
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()forObject.hasOwn(obj, key)by reusingnarrowTypeByInKeywordfor the object-operand case. - Add compiler test coverage for union narrowing, dynamic keys (no narrowing), negation/branch behavior, aliasing
Object, and compounding checks. - Add
exactOptionalPropertyTypescoverage 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. |
| 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(!...)) |
|
@daishuge You should read this: https://github.com/microsoft/TypeScript/blob/main/CONTRIBUTING.md |
Fixes #44253
Problem
Object.hasOwn(obj, key)is the modern replacement forobj.hasOwnProperty(key)and theinoperator for own-property checks, but TypeScript does not narrow the type ofobjafter the call:Approach
30 lines added to
narrowTypeByCallExpression()insrc/compiler/checker.ts. The implementation:Object.hasOwn(obj, key)calls by checking that the receiver is the globalObjectConstructornarrowTypeByInKeyword— same narrowing semantics as theinoperator, zero new narrowing logicObject.hasOwn(lib.d.tsand narrowing) #44253 about else-branch semantics (theinoperator already narrows in the else branch, but replicating that forObject.hasOwnwas not part of this scope)Before / After
Edge cases handled
const Obj = Object; Obj.hasOwn(...)works because we check the type of the receiver, not the identifier nameObject.hasOwn(obj, dynamicVar)with a non-literal key type does not narrow (same asin)containsMissingType+Record<key, unknown>intersectionif (!Object.hasOwn(obj, key))does not narrow in the truthy branch (only same-branch narrowing)if (!Object.hasOwn(o, "b")) {} else { o; }— the else branch seesassumeTrue=truevia prefix unary inversion, so narrowing applies correctlyScope
src/compiler/checker.ts: 30 lines innarrowTypeByCallExpression()tests/cases/compiler/objectHasOwnNarrowing.tswith baselinenarrowTypeByInKeyworddirectly