Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30130,6 +30130,36 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
}
// Narrow by Object.hasOwn(obj, key) — same semantics as "key in obj" but only in the true branch
if (isPropertyAccessExpression(callExpression.expression) && callExpression.arguments.length === 2) {
const callAccess = callExpression.expression;
if (isIdentifier(callAccess.name) && callAccess.name.escapedText === "hasOwn") {
const objectExpr = callAccess.expression;
const objectExprType = getTypeOfExpression(objectExpr);
// Verify that the receiver is the global ObjectConstructor
if (objectExprType.symbol && objectExprType.symbol.escapedName === "ObjectConstructor" as __String) {
const objArg = getReferenceCandidate(callExpression.arguments[0]);
const keyArg = callExpression.arguments[1];
// Case 1: reference is the object argument — narrow object type like "in" operator
if (isMatchingReference(reference, objArg)) {
const keyType = getTypeOfExpression(keyArg);
if (isTypeUsableAsPropertyName(keyType)) {
// Only narrow in the true branch (same-branch narrowing)
if (assumeTrue) {
return narrowTypeByInKeyword(type, keyType, /*assumeTrue*/ true);
}
}
}
// Case 2: reference is a property access on the object — narrow away undefined
if (containsMissingType(type) && isAccessExpression(reference) && isMatchingReference(reference.expression, objArg)) {
const keyType = getTypeOfExpression(keyArg);
if (isTypeUsableAsPropertyName(keyType) && getAccessedPropertyName(reference) === getPropertyNameFromType(keyType)) {
return getTypeWithFacts(type, assumeTrue ? TypeFacts.NEUndefined : TypeFacts.EQUndefined);
}
Comment on lines +30156 to +30158
}
}
}
}
return type;
}

Expand Down
137 changes: 137 additions & 0 deletions tests/baselines/reference/objectHasOwnNarrowing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//// [tests/cases/compiler/objectHasOwnNarrowing.ts] ////

//// [objectHasOwnNarrowing.ts]
// Basic narrowing — discriminated union member selection
type Shape = { kind: "circle"; radius: number } | { kind: "square"; side: number };
declare const s: Shape;
if (Object.hasOwn(s, "radius")) {
s; // narrowed to { kind: "circle"; radius: number }
s.radius; // ok
}

// Unknown property on object — intersects with Record<key, unknown>
declare const x: object;
if (Object.hasOwn(x, "foo")) {
x; // narrowed to object & Record<"foo", unknown>
}

// Union narrowing with partial types
type A = { tag: "a"; value: string };
type B = { tag: "b"; count: number };
type AB = A | B;
declare const ab: AB;
if (Object.hasOwn(ab, "value")) {
ab; // narrowed to A
ab.value; // string
}

// 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>
} else {
maybe.x; // should still be number | undefined, NOT never
}

// Does not narrow when key is not a literal type
declare const dynamicKey: string;
declare const o2: { [k: string]: number };
if (Object.hasOwn(o2, dynamicKey)) {
o2; // no narrowing, key is not a literal
}

// Works with aliased Object reference
const Obj = Object;
declare const o3: object;
if (Obj.hasOwn(o3, "hello")) {
o3; // narrowed to object & Record<"hello", unknown>
}

// Negation with ! — no narrowing in the false branch
declare const o4: object;
if (!Object.hasOwn(o4, "a")) {
o4; // not narrowed (we only narrow in true branch)
}

// True branch after negation doesn't narrow either (the else of the if(!...))
declare const o5: object;
if (!Object.hasOwn(o5, "b")) {
// nothing
} else {
// This is equivalent to Object.hasOwn(o5, "b") being true
// The narrowType function inverts assumeTrue for PrefixUnary !, so the else
// branch of if(!expr) sees assumeTrue=true — narrowing should work here
o5; // narrowed to object & Record<"b", unknown>
}

// Narrowing with interface types having optional and required properties
interface Config {
host: string;
port?: number;
ssl?: boolean;
}
declare const cfg: Config;
if (Object.hasOwn(cfg, "port")) {
// port is optional, so the object type has it — narrowing filters by presence
cfg; // type still Config (port is present in Config, so filter keeps it)
}

// Multiple hasOwn checks compound
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 }
}
}


//// [objectHasOwnNarrowing.js]
"use strict";
if (Object.hasOwn(s, "radius")) {
s; // narrowed to { kind: "circle"; radius: number }
s.radius; // ok
}
if (Object.hasOwn(x, "foo")) {
x; // narrowed to object & Record<"foo", unknown>
}
if (Object.hasOwn(ab, "value")) {
ab; // narrowed to A
ab.value; // string
}
if (Object.hasOwn(maybe, "x")) {
maybe; // narrowed to { x?: number } & Record<"x", unknown>
}
else {
maybe.x; // should still be number | undefined, NOT never
}
if (Object.hasOwn(o2, dynamicKey)) {
o2; // no narrowing, key is not a literal
}
// Works with aliased Object reference
const Obj = Object;
if (Obj.hasOwn(o3, "hello")) {
o3; // narrowed to object & Record<"hello", unknown>
}
if (!Object.hasOwn(o4, "a")) {
o4; // not narrowed (we only narrow in true branch)
}
if (!Object.hasOwn(o5, "b")) {
// nothing
}
else {
// This is equivalent to Object.hasOwn(o5, "b") being true
// The narrowType function inverts assumeTrue for PrefixUnary !, so the else
// branch of if(!expr) sees assumeTrue=true — narrowing should work here
o5; // narrowed to object & Record<"b", unknown>
}
if (Object.hasOwn(cfg, "port")) {
// port is optional, so the object type has it — narrowing filters by presence
cfg; // type still Config (port is present in Config, so filter keeps it)
}
if (Object.hasOwn(u, "a")) {
u; // { a: string } | { a: string; b: number }
if (Object.hasOwn(u, "b")) {
u; // { a: string; b: number }
}
}
Loading