From 90c255548856b4f16c9c664983fd2824070a8e98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:29:12 +0000 Subject: [PATCH] Add narrowing for non-union types in narrowTypeByDiscriminant Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- src/compiler/checker.ts | 20 +++++++++++++++++-- ...xhaustiveSwitchSingleEnumMember.errors.txt | 5 ++--- .../exhaustiveSwitchSingleEnumMember.js | 10 ++++------ .../exhaustiveSwitchSingleEnumMember.symbols | 5 ++--- .../exhaustiveSwitchSingleEnumMember.types | 5 ++--- .../exhaustiveSwitchSingleEnumMember.ts | 5 ++--- 6 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index cdea15b25e8..f750b810949 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -29459,6 +29459,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } propType = removeNullable && optionalChain ? getOptionalType(propType) : propType; const narrowedPropType = narrowType(propType); + // If the narrowed property type is never and the type is not a union, return never + // This handles cases where a non-union type has a single discriminant value that's been exhausted + if (narrowedPropType.flags & TypeFlags.Never && !(type.flags & TypeFlags.Union)) { + return neverType; + } return filterType(type, t => { const discriminantType = getTypeOfPropertyOrIndexSignatureOfType(t, propName) || unknownType; return !(discriminantType.flags & TypeFlags.Never) && !(narrowedPropType.flags & TypeFlags.Never) && areTypesComparable(narrowedPropType, discriminantType); @@ -29772,13 +29777,24 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return caseType; } const defaultType = filterType(type, t => !(isUnitLikeType(t) && contains(switchTypes, t.flags & TypeFlags.Undefined ? undefinedType : getRegularTypeOfLiteralType(extractUnitType(t)), (t1, t2) => isUnitType(t1) && areTypesComparable(t1, t2)))); - // Allow non-union types to narrow to never in the default case when all values are handled - if (!(type.flags & TypeFlags.Union) && isUnitLikeType(type)) { + // Allow non-union types to narrow to never when all values are handled + // This applies when caseType is never (meaning we're in a default-like position) + if (caseType.flags & TypeFlags.Never && !(type.flags & TypeFlags.Union) && isUnitLikeType(type)) { const regularType = type.flags & TypeFlags.Undefined ? undefinedType : getRegularTypeOfLiteralType(extractUnitType(type)); if (isUnitType(regularType) && contains(switchTypes, regularType, (t1, t2) => isUnitType(t1) && areTypesComparable(t1, t2))) { return neverType; } } + // Also handle single-member unions + if (caseType.flags & TypeFlags.Never && type.flags & TypeFlags.Union && (type as UnionType).types.length === 1) { + const singleType = (type as UnionType).types[0]; + if (isUnitLikeType(singleType)) { + const regularType = singleType.flags & TypeFlags.Undefined ? undefinedType : getRegularTypeOfLiteralType(extractUnitType(singleType)); + if (isUnitType(regularType) && contains(switchTypes, regularType, (t1, t2) => isUnitType(t1) && areTypesComparable(t1, t2))) { + return neverType; + } + } + } return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]); } diff --git a/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.errors.txt b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.errors.txt index ac907bcd4a0..149a4dc472b 100644 --- a/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.errors.txt +++ b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.errors.txt @@ -84,7 +84,6 @@ exhaustiveSwitchSingleEnumMember.ts(78,9): error TS2322: Type 'MultiMemberEnum.B !!! error TS2322: Type 'MultiMemberEnum.B' is not assignable to type 'never'. } - // Note: Discriminated union narrowing for single-member types is a more complex case - // that involves property access narrowing, not just direct value narrowing. - // This test focuses on direct value narrowing. + // Note: Discriminated union narrowing for single-member types requires + // narrowing through property access, which is more complex and not yet implemented. \ No newline at end of file diff --git a/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.js b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.js index 0b82213e254..5b9567b07e2 100644 --- a/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.js +++ b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.js @@ -81,9 +81,8 @@ function testIncomplete(x: MultiMemberEnum) { const n: never = x; // Error expected } -// Note: Discriminated union narrowing for single-member types is a more complex case -// that involves property access narrowing, not just direct value narrowing. -// This test focuses on direct value narrowing. +// Note: Discriminated union narrowing for single-member types requires +// narrowing through property access, which is more complex and not yet implemented. //// [exhaustiveSwitchSingleEnumMember.js] @@ -158,6 +157,5 @@ function testIncomplete(x) { // Should NOT narrow to never - B is not handled var n = x; // Error expected } -// Note: Discriminated union narrowing for single-member types is a more complex case -// that involves property access narrowing, not just direct value narrowing. -// This test focuses on direct value narrowing. +// Note: Discriminated union narrowing for single-member types requires +// narrowing through property access, which is more complex and not yet implemented. diff --git a/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.symbols b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.symbols index cd47b9b45f1..62335d1a76f 100644 --- a/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.symbols +++ b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.symbols @@ -167,7 +167,6 @@ function testIncomplete(x: MultiMemberEnum) { >x : Symbol(x, Decl(exhaustiveSwitchSingleEnumMember.ts, 71, 24)) } -// Note: Discriminated union narrowing for single-member types is a more complex case -// that involves property access narrowing, not just direct value narrowing. -// This test focuses on direct value narrowing. +// Note: Discriminated union narrowing for single-member types requires +// narrowing through property access, which is more complex and not yet implemented. diff --git a/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.types b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.types index 9d9988a4311..6a5d643664a 100644 --- a/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.types +++ b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.types @@ -249,7 +249,6 @@ function testIncomplete(x: MultiMemberEnum) { > : ^^^^^^^^^^^^^^^^^ } -// Note: Discriminated union narrowing for single-member types is a more complex case -// that involves property access narrowing, not just direct value narrowing. -// This test focuses on direct value narrowing. +// Note: Discriminated union narrowing for single-member types requires +// narrowing through property access, which is more complex and not yet implemented. diff --git a/tests/cases/compiler/exhaustiveSwitchSingleEnumMember.ts b/tests/cases/compiler/exhaustiveSwitchSingleEnumMember.ts index 7ac2cfd826b..f0de52108bf 100644 --- a/tests/cases/compiler/exhaustiveSwitchSingleEnumMember.ts +++ b/tests/cases/compiler/exhaustiveSwitchSingleEnumMember.ts @@ -79,6 +79,5 @@ function testIncomplete(x: MultiMemberEnum) { const n: never = x; // Error expected } -// Note: Discriminated union narrowing for single-member types is a more complex case -// that involves property access narrowing, not just direct value narrowing. -// This test focuses on direct value narrowing. +// Note: Discriminated union narrowing for single-member types requires +// narrowing through property access, which is more complex and not yet implemented.