From 01c92688c72d94bb6ca94fabecee2c37e11f098f Mon Sep 17 00:00:00 2001 From: daishuge Date: Sun, 5 Jul 2026 16:56:19 +0800 Subject: [PATCH] feat(checker): add Object.hasOwn() type narrowing Adds type narrowing for Object.hasOwn(obj, key) calls, reusing the existing narrowTypeByInKeyword logic since the narrowing semantics are identical to the in operator. --- src/compiler/checker.ts | 30 ++ .../reference/objectHasOwnNarrowing.js | 137 +++++++ .../reference/objectHasOwnNarrowing.symbols | 226 +++++++++++ .../reference/objectHasOwnNarrowing.types | 364 ++++++++++++++++++ .../objectHasOwnNarrowingExactOptional.js | 35 ++ ...objectHasOwnNarrowingExactOptional.symbols | 51 +++ .../objectHasOwnNarrowingExactOptional.types | 76 ++++ tests/cases/compiler/objectHasOwnNarrowing.ts | 86 +++++ .../objectHasOwnNarrowingExactOptional.ts | 23 ++ 9 files changed, 1028 insertions(+) create mode 100644 tests/baselines/reference/objectHasOwnNarrowing.js create mode 100644 tests/baselines/reference/objectHasOwnNarrowing.symbols create mode 100644 tests/baselines/reference/objectHasOwnNarrowing.types create mode 100644 tests/baselines/reference/objectHasOwnNarrowingExactOptional.js create mode 100644 tests/baselines/reference/objectHasOwnNarrowingExactOptional.symbols create mode 100644 tests/baselines/reference/objectHasOwnNarrowingExactOptional.types create mode 100644 tests/cases/compiler/objectHasOwnNarrowing.ts create mode 100644 tests/cases/compiler/objectHasOwnNarrowingExactOptional.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 0567712f11da3..b219ba8e5bcd9 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -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); + } + } + } + } + } return type; } diff --git a/tests/baselines/reference/objectHasOwnNarrowing.js b/tests/baselines/reference/objectHasOwnNarrowing.js new file mode 100644 index 0000000000000..b8c6efbf40789 --- /dev/null +++ b/tests/baselines/reference/objectHasOwnNarrowing.js @@ -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 +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 } + } +} diff --git a/tests/baselines/reference/objectHasOwnNarrowing.symbols b/tests/baselines/reference/objectHasOwnNarrowing.symbols new file mode 100644 index 0000000000000..3d167447daf02 --- /dev/null +++ b/tests/baselines/reference/objectHasOwnNarrowing.symbols @@ -0,0 +1,226 @@ +//// [tests/cases/compiler/objectHasOwnNarrowing.ts] //// + +=== objectHasOwnNarrowing.ts === +// Basic narrowing — discriminated union member selection +type Shape = { kind: "circle"; radius: number } | { kind: "square"; side: number }; +>Shape : Symbol(Shape, Decl(objectHasOwnNarrowing.ts, 0, 0)) +>kind : Symbol(kind, Decl(objectHasOwnNarrowing.ts, 1, 14)) +>radius : Symbol(radius, Decl(objectHasOwnNarrowing.ts, 1, 30)) +>kind : Symbol(kind, Decl(objectHasOwnNarrowing.ts, 1, 51)) +>side : Symbol(side, Decl(objectHasOwnNarrowing.ts, 1, 67)) + +declare const s: Shape; +>s : Symbol(s, Decl(objectHasOwnNarrowing.ts, 2, 13)) +>Shape : Symbol(Shape, Decl(objectHasOwnNarrowing.ts, 0, 0)) + +if (Object.hasOwn(s, "radius")) { +>Object.hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>s : Symbol(s, Decl(objectHasOwnNarrowing.ts, 2, 13)) + + s; // narrowed to { kind: "circle"; radius: number } +>s : Symbol(s, Decl(objectHasOwnNarrowing.ts, 2, 13)) + + s.radius; // ok +>s.radius : Symbol(radius, Decl(objectHasOwnNarrowing.ts, 1, 30)) +>s : Symbol(s, Decl(objectHasOwnNarrowing.ts, 2, 13)) +>radius : Symbol(radius, Decl(objectHasOwnNarrowing.ts, 1, 30)) +} + +// Unknown property on object — intersects with Record +declare const x: object; +>x : Symbol(x, Decl(objectHasOwnNarrowing.ts, 9, 13)) + +if (Object.hasOwn(x, "foo")) { +>Object.hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>x : Symbol(x, Decl(objectHasOwnNarrowing.ts, 9, 13)) + + x; // narrowed to object & Record<"foo", unknown> +>x : Symbol(x, Decl(objectHasOwnNarrowing.ts, 9, 13)) +} + +// Union narrowing with partial types +type A = { tag: "a"; value: string }; +>A : Symbol(A, Decl(objectHasOwnNarrowing.ts, 12, 1)) +>tag : Symbol(tag, Decl(objectHasOwnNarrowing.ts, 15, 10)) +>value : Symbol(value, Decl(objectHasOwnNarrowing.ts, 15, 20)) + +type B = { tag: "b"; count: number }; +>B : Symbol(B, Decl(objectHasOwnNarrowing.ts, 15, 37)) +>tag : Symbol(tag, Decl(objectHasOwnNarrowing.ts, 16, 10)) +>count : Symbol(count, Decl(objectHasOwnNarrowing.ts, 16, 20)) + +type AB = A | B; +>AB : Symbol(AB, Decl(objectHasOwnNarrowing.ts, 16, 37)) +>A : Symbol(A, Decl(objectHasOwnNarrowing.ts, 12, 1)) +>B : Symbol(B, Decl(objectHasOwnNarrowing.ts, 15, 37)) + +declare const ab: AB; +>ab : Symbol(ab, Decl(objectHasOwnNarrowing.ts, 18, 13)) +>AB : Symbol(AB, Decl(objectHasOwnNarrowing.ts, 16, 37)) + +if (Object.hasOwn(ab, "value")) { +>Object.hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>ab : Symbol(ab, Decl(objectHasOwnNarrowing.ts, 18, 13)) + + ab; // narrowed to A +>ab : Symbol(ab, Decl(objectHasOwnNarrowing.ts, 18, 13)) + + ab.value; // string +>ab.value : Symbol(value, Decl(objectHasOwnNarrowing.ts, 15, 20)) +>ab : Symbol(ab, Decl(objectHasOwnNarrowing.ts, 18, 13)) +>value : Symbol(value, Decl(objectHasOwnNarrowing.ts, 15, 20)) +} + +// No narrowing in else branch (same-branch only, per maintainer guidance) +declare const maybe: { x?: number }; +>maybe : Symbol(maybe, Decl(objectHasOwnNarrowing.ts, 25, 13)) +>x : Symbol(x, Decl(objectHasOwnNarrowing.ts, 25, 22)) + +if (Object.hasOwn(maybe, "x")) { +>Object.hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>maybe : Symbol(maybe, Decl(objectHasOwnNarrowing.ts, 25, 13)) + + maybe; // narrowed to { x?: number } & Record<"x", unknown> +>maybe : Symbol(maybe, Decl(objectHasOwnNarrowing.ts, 25, 13)) + +} else { + maybe.x; // should still be number | undefined, NOT never +>maybe.x : Symbol(x, Decl(objectHasOwnNarrowing.ts, 25, 22)) +>maybe : Symbol(maybe, Decl(objectHasOwnNarrowing.ts, 25, 13)) +>x : Symbol(x, Decl(objectHasOwnNarrowing.ts, 25, 22)) +} + +// Does not narrow when key is not a literal type +declare const dynamicKey: string; +>dynamicKey : Symbol(dynamicKey, Decl(objectHasOwnNarrowing.ts, 33, 13)) + +declare const o2: { [k: string]: number }; +>o2 : Symbol(o2, Decl(objectHasOwnNarrowing.ts, 34, 13)) +>k : Symbol(k, Decl(objectHasOwnNarrowing.ts, 34, 21)) + +if (Object.hasOwn(o2, dynamicKey)) { +>Object.hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>o2 : Symbol(o2, Decl(objectHasOwnNarrowing.ts, 34, 13)) +>dynamicKey : Symbol(dynamicKey, Decl(objectHasOwnNarrowing.ts, 33, 13)) + + o2; // no narrowing, key is not a literal +>o2 : Symbol(o2, Decl(objectHasOwnNarrowing.ts, 34, 13)) +} + +// Works with aliased Object reference +const Obj = Object; +>Obj : Symbol(Obj, Decl(objectHasOwnNarrowing.ts, 40, 5)) +>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) + +declare const o3: object; +>o3 : Symbol(o3, Decl(objectHasOwnNarrowing.ts, 41, 13)) + +if (Obj.hasOwn(o3, "hello")) { +>Obj.hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>Obj : Symbol(Obj, Decl(objectHasOwnNarrowing.ts, 40, 5)) +>hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>o3 : Symbol(o3, Decl(objectHasOwnNarrowing.ts, 41, 13)) + + o3; // narrowed to object & Record<"hello", unknown> +>o3 : Symbol(o3, Decl(objectHasOwnNarrowing.ts, 41, 13)) +} + +// Negation with ! — no narrowing in the false branch +declare const o4: object; +>o4 : Symbol(o4, Decl(objectHasOwnNarrowing.ts, 47, 13)) + +if (!Object.hasOwn(o4, "a")) { +>Object.hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>o4 : Symbol(o4, Decl(objectHasOwnNarrowing.ts, 47, 13)) + + o4; // not narrowed (we only narrow in true branch) +>o4 : Symbol(o4, Decl(objectHasOwnNarrowing.ts, 47, 13)) +} + +// True branch after negation doesn't narrow either (the else of the if(!...)) +declare const o5: object; +>o5 : Symbol(o5, Decl(objectHasOwnNarrowing.ts, 53, 13)) + +if (!Object.hasOwn(o5, "b")) { +>Object.hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>o5 : Symbol(o5, Decl(objectHasOwnNarrowing.ts, 53, 13)) + + // 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> +>o5 : Symbol(o5, Decl(objectHasOwnNarrowing.ts, 53, 13)) +} + +// Narrowing with interface types having optional and required properties +interface Config { +>Config : Symbol(Config, Decl(objectHasOwnNarrowing.ts, 61, 1)) + + host: string; +>host : Symbol(Config.host, Decl(objectHasOwnNarrowing.ts, 64, 18)) + + port?: number; +>port : Symbol(Config.port, Decl(objectHasOwnNarrowing.ts, 65, 17)) + + ssl?: boolean; +>ssl : Symbol(Config.ssl, Decl(objectHasOwnNarrowing.ts, 66, 18)) +} +declare const cfg: Config; +>cfg : Symbol(cfg, Decl(objectHasOwnNarrowing.ts, 69, 13)) +>Config : Symbol(Config, Decl(objectHasOwnNarrowing.ts, 61, 1)) + +if (Object.hasOwn(cfg, "port")) { +>Object.hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>cfg : Symbol(cfg, Decl(objectHasOwnNarrowing.ts, 69, 13)) + + // 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) +>cfg : Symbol(cfg, Decl(objectHasOwnNarrowing.ts, 69, 13)) +} + +// Multiple hasOwn checks compound +declare const u: { a: string } | { b: number } | { a: string; b: number }; +>u : Symbol(u, Decl(objectHasOwnNarrowing.ts, 76, 13)) +>a : Symbol(a, Decl(objectHasOwnNarrowing.ts, 76, 18)) +>b : Symbol(b, Decl(objectHasOwnNarrowing.ts, 76, 34)) +>a : Symbol(a, Decl(objectHasOwnNarrowing.ts, 76, 50)) +>b : Symbol(b, Decl(objectHasOwnNarrowing.ts, 76, 61)) + +if (Object.hasOwn(u, "a")) { +>Object.hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>u : Symbol(u, Decl(objectHasOwnNarrowing.ts, 76, 13)) + + u; // { a: string } | { a: string; b: number } +>u : Symbol(u, Decl(objectHasOwnNarrowing.ts, 76, 13)) + + if (Object.hasOwn(u, "b")) { +>Object.hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>u : Symbol(u, Decl(objectHasOwnNarrowing.ts, 76, 13)) + + u; // { a: string; b: number } +>u : Symbol(u, Decl(objectHasOwnNarrowing.ts, 76, 13)) + } +} + diff --git a/tests/baselines/reference/objectHasOwnNarrowing.types b/tests/baselines/reference/objectHasOwnNarrowing.types new file mode 100644 index 0000000000000..d621a6b33bf25 --- /dev/null +++ b/tests/baselines/reference/objectHasOwnNarrowing.types @@ -0,0 +1,364 @@ +//// [tests/cases/compiler/objectHasOwnNarrowing.ts] //// + +=== objectHasOwnNarrowing.ts === +// Basic narrowing — discriminated union member selection +type Shape = { kind: "circle"; radius: number } | { kind: "square"; side: number }; +>Shape : Shape +> : ^^^^^ +>kind : "circle" +> : ^^^^^^^^ +>radius : number +> : ^^^^^^ +>kind : "square" +> : ^^^^^^^^ +>side : number +> : ^^^^^^ + +declare const s: Shape; +>s : Shape +> : ^^^^^ + +if (Object.hasOwn(s, "radius")) { +>Object.hasOwn(s, "radius") : boolean +> : ^^^^^^^ +>Object.hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>Object : ObjectConstructor +> : ^^^^^^^^^^^^^^^^^ +>hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>s : Shape +> : ^^^^^ +>"radius" : "radius" +> : ^^^^^^^^ + + s; // narrowed to { kind: "circle"; radius: number } +>s : { kind: "circle"; radius: number; } +> : ^^^^^^^^ ^^^^^^^^^^ ^^^ + + s.radius; // ok +>s.radius : number +> : ^^^^^^ +>s : { kind: "circle"; radius: number; } +> : ^^^^^^^^ ^^^^^^^^^^ ^^^ +>radius : number +> : ^^^^^^ +} + +// Unknown property on object — intersects with Record +declare const x: object; +>x : object +> : ^^^^^^ + +if (Object.hasOwn(x, "foo")) { +>Object.hasOwn(x, "foo") : boolean +> : ^^^^^^^ +>Object.hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>Object : ObjectConstructor +> : ^^^^^^^^^^^^^^^^^ +>hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>x : object +> : ^^^^^^ +>"foo" : "foo" +> : ^^^^^ + + x; // narrowed to object & Record<"foo", unknown> +>x : object & Record<"foo", unknown> +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +} + +// Union narrowing with partial types +type A = { tag: "a"; value: string }; +>A : A +> : ^ +>tag : "a" +> : ^^^ +>value : string +> : ^^^^^^ + +type B = { tag: "b"; count: number }; +>B : B +> : ^ +>tag : "b" +> : ^^^ +>count : number +> : ^^^^^^ + +type AB = A | B; +>AB : AB +> : ^^ + +declare const ab: AB; +>ab : AB +> : ^^ + +if (Object.hasOwn(ab, "value")) { +>Object.hasOwn(ab, "value") : boolean +> : ^^^^^^^ +>Object.hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>Object : ObjectConstructor +> : ^^^^^^^^^^^^^^^^^ +>hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>ab : AB +> : ^^ +>"value" : "value" +> : ^^^^^^^ + + ab; // narrowed to A +>ab : A +> : ^ + + ab.value; // string +>ab.value : string +> : ^^^^^^ +>ab : A +> : ^ +>value : string +> : ^^^^^^ +} + +// No narrowing in else branch (same-branch only, per maintainer guidance) +declare const maybe: { x?: number }; +>maybe : { x?: number; } +> : ^^^^^^ ^^^ +>x : number | undefined +> : ^^^^^^^^^^^^^^^^^^ + +if (Object.hasOwn(maybe, "x")) { +>Object.hasOwn(maybe, "x") : boolean +> : ^^^^^^^ +>Object.hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>Object : ObjectConstructor +> : ^^^^^^^^^^^^^^^^^ +>hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>maybe : { x?: number; } +> : ^^^^^^ ^^^ +>"x" : "x" +> : ^^^ + + maybe; // narrowed to { x?: number } & Record<"x", unknown> +>maybe : { x?: number; } +> : ^^^^^^ ^^^ + +} else { + maybe.x; // should still be number | undefined, NOT never +>maybe.x : number | undefined +> : ^^^^^^^^^^^^^^^^^^ +>maybe : { x?: number; } +> : ^^^^^^ ^^^ +>x : number | undefined +> : ^^^^^^^^^^^^^^^^^^ +} + +// Does not narrow when key is not a literal type +declare const dynamicKey: string; +>dynamicKey : string +> : ^^^^^^ + +declare const o2: { [k: string]: number }; +>o2 : { [k: string]: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^ +>k : string +> : ^^^^^^ + +if (Object.hasOwn(o2, dynamicKey)) { +>Object.hasOwn(o2, dynamicKey) : boolean +> : ^^^^^^^ +>Object.hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>Object : ObjectConstructor +> : ^^^^^^^^^^^^^^^^^ +>hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>o2 : { [k: string]: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^ +>dynamicKey : string +> : ^^^^^^ + + o2; // no narrowing, key is not a literal +>o2 : { [k: string]: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^ +} + +// Works with aliased Object reference +const Obj = Object; +>Obj : ObjectConstructor +> : ^^^^^^^^^^^^^^^^^ +>Object : ObjectConstructor +> : ^^^^^^^^^^^^^^^^^ + +declare const o3: object; +>o3 : object +> : ^^^^^^ + +if (Obj.hasOwn(o3, "hello")) { +>Obj.hasOwn(o3, "hello") : boolean +> : ^^^^^^^ +>Obj.hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>Obj : ObjectConstructor +> : ^^^^^^^^^^^^^^^^^ +>hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>o3 : object +> : ^^^^^^ +>"hello" : "hello" +> : ^^^^^^^ + + o3; // narrowed to object & Record<"hello", unknown> +>o3 : object & Record<"hello", unknown> +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +} + +// Negation with ! — no narrowing in the false branch +declare const o4: object; +>o4 : object +> : ^^^^^^ + +if (!Object.hasOwn(o4, "a")) { +>!Object.hasOwn(o4, "a") : boolean +> : ^^^^^^^ +>Object.hasOwn(o4, "a") : boolean +> : ^^^^^^^ +>Object.hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>Object : ObjectConstructor +> : ^^^^^^^^^^^^^^^^^ +>hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>o4 : object +> : ^^^^^^ +>"a" : "a" +> : ^^^ + + o4; // not narrowed (we only narrow in true branch) +>o4 : object +> : ^^^^^^ +} + +// True branch after negation doesn't narrow either (the else of the if(!...)) +declare const o5: object; +>o5 : object +> : ^^^^^^ + +if (!Object.hasOwn(o5, "b")) { +>!Object.hasOwn(o5, "b") : boolean +> : ^^^^^^^ +>Object.hasOwn(o5, "b") : boolean +> : ^^^^^^^ +>Object.hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>Object : ObjectConstructor +> : ^^^^^^^^^^^^^^^^^ +>hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>o5 : object +> : ^^^^^^ +>"b" : "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> +>o5 : object & Record<"b", unknown> +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +} + +// Narrowing with interface types having optional and required properties +interface Config { + host: string; +>host : string +> : ^^^^^^ + + port?: number; +>port : number | undefined +> : ^^^^^^^^^^^^^^^^^^ + + ssl?: boolean; +>ssl : boolean | undefined +> : ^^^^^^^^^^^^^^^^^^^ +} +declare const cfg: Config; +>cfg : Config +> : ^^^^^^ + +if (Object.hasOwn(cfg, "port")) { +>Object.hasOwn(cfg, "port") : boolean +> : ^^^^^^^ +>Object.hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>Object : ObjectConstructor +> : ^^^^^^^^^^^^^^^^^ +>hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>cfg : Config +> : ^^^^^^ +>"port" : "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) +>cfg : Config +> : ^^^^^^ +} + +// Multiple hasOwn checks compound +declare const u: { a: string } | { b: number } | { a: string; b: number }; +>u : { a: string; } | { b: number; } | { a: string; b: number; } +> : ^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^ ^^^ +>a : string +> : ^^^^^^ +>b : number +> : ^^^^^^ +>a : string +> : ^^^^^^ +>b : number +> : ^^^^^^ + +if (Object.hasOwn(u, "a")) { +>Object.hasOwn(u, "a") : boolean +> : ^^^^^^^ +>Object.hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>Object : ObjectConstructor +> : ^^^^^^^^^^^^^^^^^ +>hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>u : { a: string; } | { b: number; } | { a: string; b: number; } +> : ^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^ ^^^ +>"a" : "a" +> : ^^^ + + u; // { a: string } | { a: string; b: number } +>u : { a: string; } | { a: string; b: number; } +> : ^^^^^ ^^^^^^^^^^^ ^^^^^ ^^^ + + if (Object.hasOwn(u, "b")) { +>Object.hasOwn(u, "b") : boolean +> : ^^^^^^^ +>Object.hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>Object : ObjectConstructor +> : ^^^^^^^^^^^^^^^^^ +>hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>u : { a: string; } | { a: string; b: number; } +> : ^^^^^ ^^^^^^^^^^^ ^^^^^ ^^^ +>"b" : "b" +> : ^^^ + + u; // { a: string; b: number } +>u : { a: string; b: number; } +> : ^^^^^ ^^^^^ ^^^ + } +} + diff --git a/tests/baselines/reference/objectHasOwnNarrowingExactOptional.js b/tests/baselines/reference/objectHasOwnNarrowingExactOptional.js new file mode 100644 index 0000000000000..364b75cf65be8 --- /dev/null +++ b/tests/baselines/reference/objectHasOwnNarrowingExactOptional.js @@ -0,0 +1,35 @@ +//// [tests/cases/compiler/objectHasOwnNarrowingExactOptional.ts] //// + +//// [objectHasOwnNarrowingExactOptional.ts] +// With exactOptionalPropertyTypes, accessing obj.a when property might not exist +// produces the "missing" type. Object.hasOwn should remove it. +declare const obj: { a?: string; b?: number }; +if (Object.hasOwn(obj, "a")) { + // Under exactOptionalPropertyTypes, a?: string means the property + // might be missing OR might have value string (but not undefined). + // After hasOwn confirms presence, obj.a is string. + obj.a; +} + +// Property access after hasOwn with exactOptionalPropertyTypes +interface Opts { + verbose?: boolean; + output?: string; +} +declare const opts: Opts; +if (Object.hasOwn(opts, "verbose")) { + opts.verbose; +} + + +//// [objectHasOwnNarrowingExactOptional.js] +"use strict"; +if (Object.hasOwn(obj, "a")) { + // Under exactOptionalPropertyTypes, a?: string means the property + // might be missing OR might have value string (but not undefined). + // After hasOwn confirms presence, obj.a is string. + obj.a; +} +if (Object.hasOwn(opts, "verbose")) { + opts.verbose; +} diff --git a/tests/baselines/reference/objectHasOwnNarrowingExactOptional.symbols b/tests/baselines/reference/objectHasOwnNarrowingExactOptional.symbols new file mode 100644 index 0000000000000..a63b9ac2d1aae --- /dev/null +++ b/tests/baselines/reference/objectHasOwnNarrowingExactOptional.symbols @@ -0,0 +1,51 @@ +//// [tests/cases/compiler/objectHasOwnNarrowingExactOptional.ts] //// + +=== objectHasOwnNarrowingExactOptional.ts === +// With exactOptionalPropertyTypes, accessing obj.a when property might not exist +// produces the "missing" type. Object.hasOwn should remove it. +declare const obj: { a?: string; b?: number }; +>obj : Symbol(obj, Decl(objectHasOwnNarrowingExactOptional.ts, 2, 13)) +>a : Symbol(a, Decl(objectHasOwnNarrowingExactOptional.ts, 2, 20)) +>b : Symbol(b, Decl(objectHasOwnNarrowingExactOptional.ts, 2, 32)) + +if (Object.hasOwn(obj, "a")) { +>Object.hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>obj : Symbol(obj, Decl(objectHasOwnNarrowingExactOptional.ts, 2, 13)) + + // Under exactOptionalPropertyTypes, a?: string means the property + // might be missing OR might have value string (but not undefined). + // After hasOwn confirms presence, obj.a is string. + obj.a; +>obj.a : Symbol(a, Decl(objectHasOwnNarrowingExactOptional.ts, 2, 20)) +>obj : Symbol(obj, Decl(objectHasOwnNarrowingExactOptional.ts, 2, 13)) +>a : Symbol(a, Decl(objectHasOwnNarrowingExactOptional.ts, 2, 20)) +} + +// Property access after hasOwn with exactOptionalPropertyTypes +interface Opts { +>Opts : Symbol(Opts, Decl(objectHasOwnNarrowingExactOptional.ts, 8, 1)) + + verbose?: boolean; +>verbose : Symbol(Opts.verbose, Decl(objectHasOwnNarrowingExactOptional.ts, 11, 16)) + + output?: string; +>output : Symbol(Opts.output, Decl(objectHasOwnNarrowingExactOptional.ts, 12, 22)) +} +declare const opts: Opts; +>opts : Symbol(opts, Decl(objectHasOwnNarrowingExactOptional.ts, 15, 13)) +>Opts : Symbol(Opts, Decl(objectHasOwnNarrowingExactOptional.ts, 8, 1)) + +if (Object.hasOwn(opts, "verbose")) { +>Object.hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>hasOwn : Symbol(ObjectConstructor.hasOwn, Decl(lib.es2022.object.d.ts, --, --)) +>opts : Symbol(opts, Decl(objectHasOwnNarrowingExactOptional.ts, 15, 13)) + + opts.verbose; +>opts.verbose : Symbol(Opts.verbose, Decl(objectHasOwnNarrowingExactOptional.ts, 11, 16)) +>opts : Symbol(opts, Decl(objectHasOwnNarrowingExactOptional.ts, 15, 13)) +>verbose : Symbol(Opts.verbose, Decl(objectHasOwnNarrowingExactOptional.ts, 11, 16)) +} + diff --git a/tests/baselines/reference/objectHasOwnNarrowingExactOptional.types b/tests/baselines/reference/objectHasOwnNarrowingExactOptional.types new file mode 100644 index 0000000000000..7f562c601ca98 --- /dev/null +++ b/tests/baselines/reference/objectHasOwnNarrowingExactOptional.types @@ -0,0 +1,76 @@ +//// [tests/cases/compiler/objectHasOwnNarrowingExactOptional.ts] //// + +=== objectHasOwnNarrowingExactOptional.ts === +// With exactOptionalPropertyTypes, accessing obj.a when property might not exist +// produces the "missing" type. Object.hasOwn should remove it. +declare const obj: { a?: string; b?: number }; +>obj : { a?: string; b?: number; } +> : ^^^^^^ ^^^^^^ ^^^ +>a : string | undefined +> : ^^^^^^^^^^^^^^^^^^ +>b : number | undefined +> : ^^^^^^^^^^^^^^^^^^ + +if (Object.hasOwn(obj, "a")) { +>Object.hasOwn(obj, "a") : boolean +> : ^^^^^^^ +>Object.hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>Object : ObjectConstructor +> : ^^^^^^^^^^^^^^^^^ +>hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>obj : { a?: string; b?: number; } +> : ^^^^^^ ^^^^^^ ^^^ +>"a" : "a" +> : ^^^ + + // Under exactOptionalPropertyTypes, a?: string means the property + // might be missing OR might have value string (but not undefined). + // After hasOwn confirms presence, obj.a is string. + obj.a; +>obj.a : string +> : ^^^^^^ +>obj : { a?: string; b?: number; } +> : ^^^^^^ ^^^^^^ ^^^ +>a : string +> : ^^^^^^ +} + +// Property access after hasOwn with exactOptionalPropertyTypes +interface Opts { + verbose?: boolean; +>verbose : boolean | undefined +> : ^^^^^^^^^^^^^^^^^^^ + + output?: string; +>output : string | undefined +> : ^^^^^^^^^^^^^^^^^^ +} +declare const opts: Opts; +>opts : Opts +> : ^^^^ + +if (Object.hasOwn(opts, "verbose")) { +>Object.hasOwn(opts, "verbose") : boolean +> : ^^^^^^^ +>Object.hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>Object : ObjectConstructor +> : ^^^^^^^^^^^^^^^^^ +>hasOwn : (o: object, v: PropertyKey) => boolean +> : ^ ^^ ^^ ^^ ^^^^^ +>opts : Opts +> : ^^^^ +>"verbose" : "verbose" +> : ^^^^^^^^^ + + opts.verbose; +>opts.verbose : boolean +> : ^^^^^^^ +>opts : Opts +> : ^^^^ +>verbose : boolean +> : ^^^^^^^ +} + diff --git a/tests/cases/compiler/objectHasOwnNarrowing.ts b/tests/cases/compiler/objectHasOwnNarrowing.ts new file mode 100644 index 0000000000000..c4547f10b36d7 --- /dev/null +++ b/tests/cases/compiler/objectHasOwnNarrowing.ts @@ -0,0 +1,86 @@ +// @strict: true +// @target: es2022 + +// 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 +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 } + } +} diff --git a/tests/cases/compiler/objectHasOwnNarrowingExactOptional.ts b/tests/cases/compiler/objectHasOwnNarrowingExactOptional.ts new file mode 100644 index 0000000000000..69c771201570b --- /dev/null +++ b/tests/cases/compiler/objectHasOwnNarrowingExactOptional.ts @@ -0,0 +1,23 @@ +// @strict: true +// @target: es2022 +// @exactOptionalPropertyTypes: true + +// With exactOptionalPropertyTypes, accessing obj.a when property might not exist +// produces the "missing" type. Object.hasOwn should remove it. +declare const obj: { a?: string; b?: number }; +if (Object.hasOwn(obj, "a")) { + // Under exactOptionalPropertyTypes, a?: string means the property + // might be missing OR might have value string (but not undefined). + // After hasOwn confirms presence, obj.a is string. + obj.a; +} + +// Property access after hasOwn with exactOptionalPropertyTypes +interface Opts { + verbose?: boolean; + output?: string; +} +declare const opts: Opts; +if (Object.hasOwn(opts, "verbose")) { + opts.verbose; +}