From c3807802371a89998887740182ee4a348b400b30 Mon Sep 17 00:00:00 2001 From: Prathmesh Ravindra Salunkhe Date: Wed, 1 Oct 2025 12:58:40 +0530 Subject: [PATCH 1/2] Add test for discriminant property order-independence and update baselines --- .../discriminantOrderIndependence.js | 61 +++++++ .../discriminantOrderIndependence.symbols | 106 ++++++++++++ .../discriminantOrderIndependence.types | 162 ++++++++++++++++++ .../compiler/discriminantOrderIndependence.ts | 41 +++++ 4 files changed, 370 insertions(+) create mode 100644 tests/baselines/reference/discriminantOrderIndependence.js create mode 100644 tests/baselines/reference/discriminantOrderIndependence.symbols create mode 100644 tests/baselines/reference/discriminantOrderIndependence.types create mode 100644 tests/cases/compiler/discriminantOrderIndependence.ts diff --git a/tests/baselines/reference/discriminantOrderIndependence.js b/tests/baselines/reference/discriminantOrderIndependence.js new file mode 100644 index 0000000000000..0dcd9850e352f --- /dev/null +++ b/tests/baselines/reference/discriminantOrderIndependence.js @@ -0,0 +1,61 @@ +//// [tests/cases/compiler/discriminantOrderIndependence.ts] //// + +//// [discriminantOrderIndependence.ts] +interface A { + subType: "b"; + type: "a"; +} + +declare let order1: + | { type: "1" } + | A + | { type: "2" } + | { type: "3" } + | undefined; + +// Should NOT error: 'order1' is possibly 'undefined' after the guard +if (order1 && order1.type === "a") { + order1.type; // Should be OK +} + +interface B { + subType: "b"; + type: "a"; +} + +declare let order2: + | { type: "1" } + | { type: "2" } + | { type: "3" } + | B + | undefined; + +// Should NOT error: 'order2' is possibly 'undefined' after the guard +if (order2 && order2.type === "a") { + order2.type; // Should be OK +} + +// Also test with !. type assertion +if (order1 && order1.type === "a") { + order1.type; // Should be OK +} +if (order2 && order2.type === "a") { + order2.type; // Should be OK +} + +//// [discriminantOrderIndependence.js] +// Should NOT error: 'order1' is possibly 'undefined' after the guard +if (order1 && order1.type === "a") { + order1.type; // Should be OK +} +// Should NOT error: 'order2' is possibly 'undefined' after the guard +if (order2 && order2.type === "a") { + order2.type; // Should be OK +} +// Also test with !. type assertion +if (order1 && order1.type === "a") { + order1.type; // Should be OK +} +if (order2 && order2.type === "a") { + order2.type; // Should be OK +} diff --git a/tests/baselines/reference/discriminantOrderIndependence.symbols b/tests/baselines/reference/discriminantOrderIndependence.symbols new file mode 100644 index 0000000000000..e470f892e5268 --- /dev/null +++ b/tests/baselines/reference/discriminantOrderIndependence.symbols @@ -0,0 +1,106 @@ +//// [tests/cases/compiler/discriminantOrderIndependence.ts] //// + +=== discriminantOrderIndependence.ts === +interface A { +>A : Symbol(A, Decl(discriminantOrderIndependence.ts, 0, 0)) + + subType: "b"; +>subType : Symbol(A.subType, Decl(discriminantOrderIndependence.ts, 0, 13)) + + type: "a"; +>type : Symbol(A.type, Decl(discriminantOrderIndependence.ts, 1, 17)) +} + +declare let order1: +>order1 : Symbol(order1, Decl(discriminantOrderIndependence.ts, 5, 11)) + + | { type: "1" } +>type : Symbol(type, Decl(discriminantOrderIndependence.ts, 6, 7)) + + | A +>A : Symbol(A, Decl(discriminantOrderIndependence.ts, 0, 0)) + + | { type: "2" } +>type : Symbol(type, Decl(discriminantOrderIndependence.ts, 8, 7)) + + | { type: "3" } +>type : Symbol(type, Decl(discriminantOrderIndependence.ts, 9, 7)) + + | undefined; + +// Should NOT error: 'order1' is possibly 'undefined' after the guard +if (order1 && order1.type === "a") { +>order1 : Symbol(order1, Decl(discriminantOrderIndependence.ts, 5, 11)) +>order1.type : Symbol(type, Decl(discriminantOrderIndependence.ts, 1, 17), Decl(discriminantOrderIndependence.ts, 6, 7), Decl(discriminantOrderIndependence.ts, 8, 7), Decl(discriminantOrderIndependence.ts, 9, 7)) +>order1 : Symbol(order1, Decl(discriminantOrderIndependence.ts, 5, 11)) +>type : Symbol(type, Decl(discriminantOrderIndependence.ts, 1, 17), Decl(discriminantOrderIndependence.ts, 6, 7), Decl(discriminantOrderIndependence.ts, 8, 7), Decl(discriminantOrderIndependence.ts, 9, 7)) + + order1.type; // Should be OK +>order1.type : Symbol(A.type, Decl(discriminantOrderIndependence.ts, 1, 17)) +>order1 : Symbol(order1, Decl(discriminantOrderIndependence.ts, 5, 11)) +>type : Symbol(A.type, Decl(discriminantOrderIndependence.ts, 1, 17)) +} + +interface B { +>B : Symbol(B, Decl(discriminantOrderIndependence.ts, 15, 1)) + + subType: "b"; +>subType : Symbol(B.subType, Decl(discriminantOrderIndependence.ts, 17, 13)) + + type: "a"; +>type : Symbol(B.type, Decl(discriminantOrderIndependence.ts, 18, 17)) +} + +declare let order2: +>order2 : Symbol(order2, Decl(discriminantOrderIndependence.ts, 22, 11)) + + | { type: "1" } +>type : Symbol(type, Decl(discriminantOrderIndependence.ts, 23, 7)) + + | { type: "2" } +>type : Symbol(type, Decl(discriminantOrderIndependence.ts, 24, 7)) + + | { type: "3" } +>type : Symbol(type, Decl(discriminantOrderIndependence.ts, 25, 7)) + + | B +>B : Symbol(B, Decl(discriminantOrderIndependence.ts, 15, 1)) + + | undefined; + +// Should NOT error: 'order2' is possibly 'undefined' after the guard +if (order2 && order2.type === "a") { +>order2 : Symbol(order2, Decl(discriminantOrderIndependence.ts, 22, 11)) +>order2.type : Symbol(type, Decl(discriminantOrderIndependence.ts, 18, 17), Decl(discriminantOrderIndependence.ts, 23, 7), Decl(discriminantOrderIndependence.ts, 24, 7), Decl(discriminantOrderIndependence.ts, 25, 7)) +>order2 : Symbol(order2, Decl(discriminantOrderIndependence.ts, 22, 11)) +>type : Symbol(type, Decl(discriminantOrderIndependence.ts, 18, 17), Decl(discriminantOrderIndependence.ts, 23, 7), Decl(discriminantOrderIndependence.ts, 24, 7), Decl(discriminantOrderIndependence.ts, 25, 7)) + + order2.type; // Should be OK +>order2.type : Symbol(B.type, Decl(discriminantOrderIndependence.ts, 18, 17)) +>order2 : Symbol(order2, Decl(discriminantOrderIndependence.ts, 22, 11)) +>type : Symbol(B.type, Decl(discriminantOrderIndependence.ts, 18, 17)) +} + +// Also test with !. type assertion +if (order1 && order1.type === "a") { +>order1 : Symbol(order1, Decl(discriminantOrderIndependence.ts, 5, 11)) +>order1.type : Symbol(type, Decl(discriminantOrderIndependence.ts, 1, 17), Decl(discriminantOrderIndependence.ts, 6, 7), Decl(discriminantOrderIndependence.ts, 8, 7), Decl(discriminantOrderIndependence.ts, 9, 7)) +>order1 : Symbol(order1, Decl(discriminantOrderIndependence.ts, 5, 11)) +>type : Symbol(type, Decl(discriminantOrderIndependence.ts, 1, 17), Decl(discriminantOrderIndependence.ts, 6, 7), Decl(discriminantOrderIndependence.ts, 8, 7), Decl(discriminantOrderIndependence.ts, 9, 7)) + + order1.type; // Should be OK +>order1.type : Symbol(A.type, Decl(discriminantOrderIndependence.ts, 1, 17)) +>order1 : Symbol(order1, Decl(discriminantOrderIndependence.ts, 5, 11)) +>type : Symbol(A.type, Decl(discriminantOrderIndependence.ts, 1, 17)) +} +if (order2 && order2.type === "a") { +>order2 : Symbol(order2, Decl(discriminantOrderIndependence.ts, 22, 11)) +>order2.type : Symbol(type, Decl(discriminantOrderIndependence.ts, 18, 17), Decl(discriminantOrderIndependence.ts, 23, 7), Decl(discriminantOrderIndependence.ts, 24, 7), Decl(discriminantOrderIndependence.ts, 25, 7)) +>order2 : Symbol(order2, Decl(discriminantOrderIndependence.ts, 22, 11)) +>type : Symbol(type, Decl(discriminantOrderIndependence.ts, 18, 17), Decl(discriminantOrderIndependence.ts, 23, 7), Decl(discriminantOrderIndependence.ts, 24, 7), Decl(discriminantOrderIndependence.ts, 25, 7)) + + order2.type; // Should be OK +>order2.type : Symbol(B.type, Decl(discriminantOrderIndependence.ts, 18, 17)) +>order2 : Symbol(order2, Decl(discriminantOrderIndependence.ts, 22, 11)) +>type : Symbol(B.type, Decl(discriminantOrderIndependence.ts, 18, 17)) +} diff --git a/tests/baselines/reference/discriminantOrderIndependence.types b/tests/baselines/reference/discriminantOrderIndependence.types new file mode 100644 index 0000000000000..d561f4cbefc21 --- /dev/null +++ b/tests/baselines/reference/discriminantOrderIndependence.types @@ -0,0 +1,162 @@ +//// [tests/cases/compiler/discriminantOrderIndependence.ts] //// + +=== discriminantOrderIndependence.ts === +interface A { + subType: "b"; +>subType : "b" +> : ^^^ + + type: "a"; +>type : "a" +> : ^^^ +} + +declare let order1: +>order1 : A | { type: "1"; } | { type: "2"; } | { type: "3"; } +> : ^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^ + + | { type: "1" } +>type : "1" +> : ^^^ + + | A + | { type: "2" } +>type : "2" +> : ^^^ + + | { type: "3" } +>type : "3" +> : ^^^ + + | undefined; + +// Should NOT error: 'order1' is possibly 'undefined' after the guard +if (order1 && order1.type === "a") { +>order1 && order1.type === "a" : boolean +> : ^^^^^^^ +>order1 : A | { type: "1"; } | { type: "2"; } | { type: "3"; } +> : ^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^ +>order1.type === "a" : boolean +> : ^^^^^^^ +>order1.type : "a" | "1" | "2" | "3" +> : ^^^^^^^^^^^^^^^^^^^^^ +>order1 : A | { type: "1"; } | { type: "2"; } | { type: "3"; } +> : ^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^ +>type : "a" | "1" | "2" | "3" +> : ^^^^^^^^^^^^^^^^^^^^^ +>"a" : "a" +> : ^^^ + + order1.type; // Should be OK +>order1.type : "a" +> : ^^^ +>order1 : A +> : ^ +>type : "a" +> : ^^^ +} + +interface B { + subType: "b"; +>subType : "b" +> : ^^^ + + type: "a"; +>type : "a" +> : ^^^ +} + +declare let order2: +>order2 : B | { type: "1"; } | { type: "2"; } | { type: "3"; } +> : ^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^ + + | { type: "1" } +>type : "1" +> : ^^^ + + | { type: "2" } +>type : "2" +> : ^^^ + + | { type: "3" } +>type : "3" +> : ^^^ + + | B + | undefined; + +// Should NOT error: 'order2' is possibly 'undefined' after the guard +if (order2 && order2.type === "a") { +>order2 && order2.type === "a" : boolean +> : ^^^^^^^ +>order2 : B | { type: "1"; } | { type: "2"; } | { type: "3"; } +> : ^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^ +>order2.type === "a" : boolean +> : ^^^^^^^ +>order2.type : "a" | "1" | "2" | "3" +> : ^^^^^^^^^^^^^^^^^^^^^ +>order2 : B | { type: "1"; } | { type: "2"; } | { type: "3"; } +> : ^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^ +>type : "a" | "1" | "2" | "3" +> : ^^^^^^^^^^^^^^^^^^^^^ +>"a" : "a" +> : ^^^ + + order2.type; // Should be OK +>order2.type : "a" +> : ^^^ +>order2 : B +> : ^ +>type : "a" +> : ^^^ +} + +// Also test with !. type assertion +if (order1 && order1.type === "a") { +>order1 && order1.type === "a" : boolean +> : ^^^^^^^ +>order1 : A | { type: "1"; } | { type: "2"; } | { type: "3"; } +> : ^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^ +>order1.type === "a" : boolean +> : ^^^^^^^ +>order1.type : "a" | "1" | "2" | "3" +> : ^^^^^^^^^^^^^^^^^^^^^ +>order1 : A | { type: "1"; } | { type: "2"; } | { type: "3"; } +> : ^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^ +>type : "a" | "1" | "2" | "3" +> : ^^^^^^^^^^^^^^^^^^^^^ +>"a" : "a" +> : ^^^ + + order1.type; // Should be OK +>order1.type : "a" +> : ^^^ +>order1 : A +> : ^ +>type : "a" +> : ^^^ +} +if (order2 && order2.type === "a") { +>order2 && order2.type === "a" : boolean +> : ^^^^^^^ +>order2 : B | { type: "1"; } | { type: "2"; } | { type: "3"; } +> : ^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^ +>order2.type === "a" : boolean +> : ^^^^^^^ +>order2.type : "a" | "1" | "2" | "3" +> : ^^^^^^^^^^^^^^^^^^^^^ +>order2 : B | { type: "1"; } | { type: "2"; } | { type: "3"; } +> : ^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^ +>type : "a" | "1" | "2" | "3" +> : ^^^^^^^^^^^^^^^^^^^^^ +>"a" : "a" +> : ^^^ + + order2.type; // Should be OK +>order2.type : "a" +> : ^^^ +>order2 : B +> : ^ +>type : "a" +> : ^^^ +} diff --git a/tests/cases/compiler/discriminantOrderIndependence.ts b/tests/cases/compiler/discriminantOrderIndependence.ts new file mode 100644 index 0000000000000..f43924efdf0b8 --- /dev/null +++ b/tests/cases/compiler/discriminantOrderIndependence.ts @@ -0,0 +1,41 @@ +interface A { + subType: "b"; + type: "a"; +} + +declare let order1: + | { type: "1" } + | A + | { type: "2" } + | { type: "3" } + | undefined; + +// Should NOT error: 'order1' is possibly 'undefined' after the guard +if (order1 && order1.type === "a") { + order1.type; // Should be OK +} + +interface B { + subType: "b"; + type: "a"; +} + +declare let order2: + | { type: "1" } + | { type: "2" } + | { type: "3" } + | B + | undefined; + +// Should NOT error: 'order2' is possibly 'undefined' after the guard +if (order2 && order2.type === "a") { + order2.type; // Should be OK +} + +// Also test with !. type assertion +if (order1 && order1.type === "a") { + order1.type; // Should be OK +} +if (order2 && order2.type === "a") { + order2.type; // Should be OK +} \ No newline at end of file From 3e2914fe7d297ad98b54708c16b38c391383b564 Mon Sep 17 00:00:00 2001 From: Prathmesh Ravindra Salunkhe Date: Wed, 1 Oct 2025 20:30:31 +0530 Subject: [PATCH 2/2] Fix: update checker.ts for order-independent discriminant property selection --- src/compiler/checker.ts | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index cbdbd55660bcd..ab69a9014f726 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -27911,7 +27911,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // constituent types keyed by the literal types of the property by that name in each constituent type. function getKeyPropertyName(unionType: UnionType): __String | undefined { const types = unionType.types; - // We only construct maps for unions with many non-primitive constituents. if ( types.length < 10 || getObjectFlags(unionType) & ObjectFlags.PrimitiveUnion || countWhere(types, t => !!(t.flags & (TypeFlags.Object | TypeFlags.InstantiableNonPrimitive))) < 10 @@ -27919,15 +27918,37 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return undefined; } if (unionType.keyPropertyName === undefined) { - // The candidate key property name is the name of the first property with a unit type in one of the - // constituent types. - const keyPropertyName = forEach(types, t => - t.flags & (TypeFlags.Object | TypeFlags.InstantiableNonPrimitive) ? - forEach(getPropertiesOfType(t), p => isUnitType(getTypeOfSymbol(p)) ? p.escapedName : undefined) : - undefined); - const mapByKeyProperty = keyPropertyName && mapTypesByKeyProperty(types, keyPropertyName); - unionType.keyPropertyName = mapByKeyProperty ? keyPropertyName : "" as __String; - unionType.constituentMap = mapByKeyProperty; + // Map property name to count of object types where it's a unit (literal) type + const propertyCounts: Map = new Map(); + let objectTypeCount = 0; + + for (const t of types) { + if (t.flags & (TypeFlags.Object | TypeFlags.InstantiableNonPrimitive)) { + objectTypeCount++; + for (const p of getPropertiesOfType(t)) { + if (isUnitType(getTypeOfSymbol(p))) { + const name = p.escapedName as string; + propertyCounts.set(name, (propertyCounts.get(name) || 0) + 1); + } + } + } + } + + // Choose property present with unit type in ALL object members, or the most common + let bestPropertyName: string | undefined; + let bestCount = 0; + + for (const [name, count] of propertyCounts.entries()) { + // Prefer property present in all object types, otherwise pick the most frequent + if ((count > bestCount) || (count === objectTypeCount && count >= bestCount)) { + bestPropertyName = name; + bestCount = count; + } + } + + const mapByKeyProperty = bestPropertyName && mapTypesByKeyProperty(types, bestPropertyName as __String); + unionType.keyPropertyName = mapByKeyProperty ? bestPropertyName as __String : "" as __String; + unionType.constituentMap = typeof mapByKeyProperty === "object" ? mapByKeyProperty : undefined; } return (unionType.keyPropertyName as string).length ? unionType.keyPropertyName : undefined; }