From 8d3be0e5106a7f5f686a8de626e07c7256d716a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 23 Jul 2025 20:42:25 +0000 Subject: [PATCH 1/4] Initial plan From 5d94666678f39fa7bfe8b1f0d9ad7afabfdb255f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 23 Jul 2025 21:04:58 +0000 Subject: [PATCH 2/4] Implement fix for exhaustiveness checking on non-union types Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- src/compiler/checker.ts | 214 +++++++------- .../exhaustiveChecksForNonUnionTypes.js | 167 +++++++++++ .../exhaustiveChecksForNonUnionTypes.symbols | 181 ++++++++++++ .../exhaustiveChecksForNonUnionTypes.types | 266 ++++++++++++++++++ .../exhaustiveChecksForNonUnionTypes.ts | 87 ++++++ 5 files changed, 818 insertions(+), 97 deletions(-) create mode 100644 tests/baselines/reference/exhaustiveChecksForNonUnionTypes.js create mode 100644 tests/baselines/reference/exhaustiveChecksForNonUnionTypes.symbols create mode 100644 tests/baselines/reference/exhaustiveChecksForNonUnionTypes.types create mode 100644 tests/cases/compiler/exhaustiveChecksForNonUnionTypes.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index aabb6dd9301fc..7c28a2c6045d5 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -29380,42 +29380,59 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return undefined; } - function getDiscriminantPropertyAccess(expr: Expression, computedType: Type) { - // As long as the computed type is a subset of the declared type, we use the full declared type to detect - // a discriminant property. In cases where the computed type isn't a subset, e.g because of a preceding type - // predicate narrowing, we use the actual computed type. - if (declaredType.flags & TypeFlags.Union || computedType.flags & TypeFlags.Union) { - const access = getCandidateDiscriminantPropertyAccess(expr); - if (access) { - const name = getAccessedPropertyName(access); - if (name) { - const type = declaredType.flags & TypeFlags.Union && isTypeSubsetOf(computedType, declaredType) ? declaredType : computedType; - if (isDiscriminantProperty(type, name)) { - return access; - } - } - } - } - return undefined; - } - - function narrowTypeByDiscriminant(type: Type, access: AccessExpression | BindingElement | ParameterDeclaration, narrowType: (t: Type) => Type): Type { - const propName = getAccessedPropertyName(access); - if (propName === undefined) { - return type; - } - const optionalChain = isOptionalChain(access); - const removeNullable = strictNullChecks && (optionalChain || isNonNullAccess(access)) && maybeTypeOfKind(type, TypeFlags.Nullable); - let propType = getTypeOfPropertyOfType(removeNullable ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type, propName); - if (!propType) { - return type; - } - propType = removeNullable && optionalChain ? getOptionalType(propType) : propType; - const narrowedPropType = narrowType(propType); - return filterType(type, t => { - const discriminantType = getTypeOfPropertyOrIndexSignatureOfType(t, propName) || unknownType; - return !(discriminantType.flags & TypeFlags.Never) && !(narrowedPropType.flags & TypeFlags.Never) && areTypesComparable(narrowedPropType, discriminantType); - }); + function getDiscriminantPropertyAccess(expr: Expression, computedType: Type) { + // As long as the computed type is a subset of the declared type, we use the full declared type to detect + // a discriminant property. In cases where the computed type isn't a subset, e.g because of a preceding type + // predicate narrowing, we use the actual computed type. + if (declaredType.flags & TypeFlags.Union || computedType.flags & TypeFlags.Union) { + const access = getCandidateDiscriminantPropertyAccess(expr); + if (access) { + const name = getAccessedPropertyName(access); + if (name) { + const type = declaredType.flags & TypeFlags.Union && isTypeSubsetOf(computedType, declaredType) ? declaredType : computedType; + if (isDiscriminantProperty(type, name)) { + return access; + } + } + } + } + // Fix for #23572: Allow discriminant property narrowing for non-union types + // This enables narrowing to never when all possibilities are eliminated + else { + const access = getCandidateDiscriminantPropertyAccess(expr); + if (access) { + const name = getAccessedPropertyName(access); + if (name) { + // For non-union types, check if the property exists and has a literal type + const type = declaredType.flags & TypeFlags.Union && isTypeSubsetOf(computedType, declaredType) ? declaredType : computedType; + const propType = getTypeOfPropertyOfType(type, name); + if (propType && isUnitLikeType(propType)) { + return access; + } + } + } + } + return undefined; + } + + function narrowTypeByDiscriminant(type: Type, access: AccessExpression | BindingElement | ParameterDeclaration, narrowType: (t: Type) => Type): Type { + const propName = getAccessedPropertyName(access); + if (propName === undefined) { + return type; + } + const optionalChain = isOptionalChain(access); + const removeNullable = strictNullChecks && (optionalChain || isNonNullAccess(access)) && maybeTypeOfKind(type, TypeFlags.Nullable); + let propType = getTypeOfPropertyOfType(removeNullable ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type, propName); + if (!propType) { + return type; + } + propType = removeNullable && optionalChain ? getOptionalType(propType) : propType; + const narrowedPropType = narrowType(propType); + return filterType(type, t => { + const discriminantType = getTypeOfPropertyOrIndexSignatureOfType(t, propName) || unknownType; + const result = !(discriminantType.flags & TypeFlags.Never) && !(narrowedPropType.flags & TypeFlags.Never) && areTypesComparable(narrowedPropType, discriminantType); + return result; + }); } function narrowTypeByDiscriminantProperty(type: Type, access: AccessExpression | BindingElement | ParameterDeclaration, operator: SyntaxKind, value: Expression, assumeTrue: boolean) { @@ -29618,42 +29635,43 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return removeNullable ? getAdjustedTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type; } - function narrowTypeByEquality(type: Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): Type { - if (type.flags & TypeFlags.Any) { - return type; - } - if (operator === SyntaxKind.ExclamationEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) { - assumeTrue = !assumeTrue; - } - const valueType = getTypeOfExpression(value); - const doubleEquals = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken; - if (valueType.flags & TypeFlags.Nullable) { - if (!strictNullChecks) { - return type; - } - const facts = doubleEquals ? - assumeTrue ? TypeFacts.EQUndefinedOrNull : TypeFacts.NEUndefinedOrNull : - valueType.flags & TypeFlags.Null ? - assumeTrue ? TypeFacts.EQNull : TypeFacts.NENull : - assumeTrue ? TypeFacts.EQUndefined : TypeFacts.NEUndefined; - return getAdjustedTypeWithFacts(type, facts); - } - if (assumeTrue) { - if (!doubleEquals && (type.flags & TypeFlags.Unknown || someType(type, isEmptyAnonymousObjectType))) { - if (valueType.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive) || isEmptyAnonymousObjectType(valueType)) { - return valueType; - } - if (valueType.flags & TypeFlags.Object) { - return nonPrimitiveType; - } - } - const filteredType = filterType(type, t => areTypesComparable(t, valueType) || doubleEquals && isCoercibleUnderDoubleEquals(t, valueType)); - return replacePrimitivesWithLiterals(filteredType, valueType); - } - if (isUnitType(valueType)) { - return filterType(type, t => !(isUnitLikeType(t) && areTypesComparable(t, valueType))); - } - return type; + function narrowTypeByEquality(type: Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): Type { + if (type.flags & TypeFlags.Any) { + return type; + } + if (operator === SyntaxKind.ExclamationEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) { + assumeTrue = !assumeTrue; + } + const valueType = getTypeOfExpression(value); + const doubleEquals = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken; + if (valueType.flags & TypeFlags.Nullable) { + if (!strictNullChecks) { + return type; + } + const facts = doubleEquals ? + assumeTrue ? TypeFacts.EQUndefinedOrNull : TypeFacts.NEUndefinedOrNull : + valueType.flags & TypeFlags.Null ? + assumeTrue ? TypeFacts.EQNull : TypeFacts.NENull : + assumeTrue ? TypeFacts.EQUndefined : TypeFacts.NEUndefined; + return getAdjustedTypeWithFacts(type, facts); + } + if (assumeTrue) { + if (!doubleEquals && (type.flags & TypeFlags.Unknown || someType(type, isEmptyAnonymousObjectType))) { + if (valueType.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive) || isEmptyAnonymousObjectType(valueType)) { + return valueType; + } + if (valueType.flags & TypeFlags.Object) { + return nonPrimitiveType; + } + } + const filteredType = filterType(type, t => areTypesComparable(t, valueType) || doubleEquals && isCoercibleUnderDoubleEquals(t, valueType)); + return replacePrimitivesWithLiterals(filteredType, valueType); + } + if (isUnitType(valueType)) { + const result = filterType(type, t => !(isUnitLikeType(t) && areTypesComparable(t, valueType))); + return result; + } + return type; } function narrowTypeByTypeof(type: Type, typeOfExpr: TypeOfExpression, operator: SyntaxKind, literal: LiteralExpression, assumeTrue: boolean): Type { @@ -39250,31 +39268,33 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return links.isExhaustive; } - function computeExhaustiveSwitchStatement(node: SwitchStatement): boolean { - if (node.expression.kind === SyntaxKind.TypeOfExpression) { - const witnesses = getSwitchClauseTypeOfWitnesses(node); - if (!witnesses) { - return false; - } - const operandConstraint = getBaseConstraintOrType(checkExpressionCached((node.expression as TypeOfExpression).expression)); - // Get the not-equal flags for all handled cases. - const notEqualFacts = getNotEqualFactsFromTypeofSwitch(0, 0, witnesses); - if (operandConstraint.flags & TypeFlags.AnyOrUnknown) { - // We special case the top types to be exhaustive when all cases are handled. - return (TypeFacts.AllTypeofNE & notEqualFacts) === TypeFacts.AllTypeofNE; - } - // A missing not-equal flag indicates that the type wasn't handled by some case. - return !someType(operandConstraint, t => getTypeFacts(t, notEqualFacts) === notEqualFacts); - } - const type = getBaseConstraintOrType(checkExpressionCached(node.expression)); - if (!isLiteralType(type)) { - return false; - } - const switchTypes = getSwitchClauseTypes(node); - if (!switchTypes.length || some(switchTypes, isNeitherUnitTypeNorNever)) { - return false; - } - return eachTypeContainedIn(mapType(type, getRegularTypeOfLiteralType), switchTypes); + function computeExhaustiveSwitchStatement(node: SwitchStatement): boolean { + if (node.expression.kind === SyntaxKind.TypeOfExpression) { + const witnesses = getSwitchClauseTypeOfWitnesses(node); + if (!witnesses) { + return false; + } + const operandConstraint = getBaseConstraintOrType(checkExpressionCached((node.expression as TypeOfExpression).expression)); + // Get the not-equal flags for all handled cases. + const notEqualFacts = getNotEqualFactsFromTypeofSwitch(0, 0, witnesses); + if (operandConstraint.flags & TypeFlags.AnyOrUnknown) { + // We special case the top types to be exhaustive when all cases are handled. + return (TypeFacts.AllTypeofNE & notEqualFacts) === TypeFacts.AllTypeofNE; + } + // A missing not-equal flag indicates that the type wasn't handled by some case. + return !someType(operandConstraint, t => getTypeFacts(t, notEqualFacts) === notEqualFacts); + } + const type = getBaseConstraintOrType(checkExpressionCached(node.expression)); + if (!isLiteralType(type)) { + return false; + } + const switchTypes = getSwitchClauseTypes(node); + if (!switchTypes.length || some(switchTypes, isNeitherUnitTypeNorNever)) { + return false; + } + const mappedType = mapType(type, getRegularTypeOfLiteralType); + const result = eachTypeContainedIn(mappedType, switchTypes); + return result; } function functionHasImplicitReturn(func: FunctionLikeDeclaration) { diff --git a/tests/baselines/reference/exhaustiveChecksForNonUnionTypes.js b/tests/baselines/reference/exhaustiveChecksForNonUnionTypes.js new file mode 100644 index 0000000000000..3d64fb5f1c955 --- /dev/null +++ b/tests/baselines/reference/exhaustiveChecksForNonUnionTypes.js @@ -0,0 +1,167 @@ +//// [tests/cases/compiler/exhaustiveChecksForNonUnionTypes.ts] //// + +//// [exhaustiveChecksForNonUnionTypes.ts] +// Basic case: narrowing non-union types to never +function testBasicNarrowing(obj: { name: "bob" }) { + if (obj.name === "bob") { + // obj.name is "bob" + } else { + // obj should be narrowed to never since { name: "bob" } with name !== "bob" is impossible + const n: never = obj; + } +} + +// Single enum member case +enum SingleAction { + INCREMENT = 'INCREMENT' +} + +interface IIncrement { + payload: {}; + type: SingleAction.INCREMENT; +} + +function testSingleEnumSwitch(action: IIncrement) { + switch (action.type) { + case SingleAction.INCREMENT: + return 1; + } + + // action should be narrowed to never since all cases are handled + const n: never = action; +} + +// Single literal type case (should already work) +function testSingleLiteral(x: "a") { + if (x === "a") { + // x is "a" + } else { + // x should be never + const n: never = x; + } +} + +// Single enum value case +enum Single { A = "a" } + +function testSingleEnum(x: Single) { + if (x === Single.A) { + // x is Single.A + } else { + // x should be never + const n: never = x; + } +} + +// More complex object with multiple literal properties +function testComplexObject(obj: { type: "user", status: "active" }) { + if (obj.type === "user") { + if (obj.status === "active") { + // Both properties match + } else { + // obj.status !== "active" but obj: { type: "user", status: "active" } - impossible + const n: never = obj; + } + } else { + // obj.type !== "user" but obj: { type: "user", status: "active" } - impossible + const n: never = obj; + } +} + +// Switch statement with single case (original issue) +enum ActionTypes { + INCREMENT = 'INCREMENT', +} + +interface IAction { + type: ActionTypes.INCREMENT; +} + +function testOriginalIssue(action: IAction) { + switch (action.type) { + case ActionTypes.INCREMENT: + return 1; + } + + // This was the original issue - action should be never but wasn't + const n: never = action; +} + +//// [exhaustiveChecksForNonUnionTypes.js] +"use strict"; +// Basic case: narrowing non-union types to never +function testBasicNarrowing(obj) { + if (obj.name === "bob") { + // obj.name is "bob" + } + else { + // obj should be narrowed to never since { name: "bob" } with name !== "bob" is impossible + var n = obj; + } +} +// Single enum member case +var SingleAction; +(function (SingleAction) { + SingleAction["INCREMENT"] = "INCREMENT"; +})(SingleAction || (SingleAction = {})); +function testSingleEnumSwitch(action) { + switch (action.type) { + case SingleAction.INCREMENT: + return 1; + } + // action should be narrowed to never since all cases are handled + var n = action; +} +// Single literal type case (should already work) +function testSingleLiteral(x) { + if (x === "a") { + // x is "a" + } + else { + // x should be never + var n = x; + } +} +// Single enum value case +var Single; +(function (Single) { + Single["A"] = "a"; +})(Single || (Single = {})); +function testSingleEnum(x) { + if (x === Single.A) { + // x is Single.A + } + else { + // x should be never + var n = x; + } +} +// More complex object with multiple literal properties +function testComplexObject(obj) { + if (obj.type === "user") { + if (obj.status === "active") { + // Both properties match + } + else { + // obj.status !== "active" but obj: { type: "user", status: "active" } - impossible + var n = obj; + } + } + else { + // obj.type !== "user" but obj: { type: "user", status: "active" } - impossible + var n = obj; + } +} +// Switch statement with single case (original issue) +var ActionTypes; +(function (ActionTypes) { + ActionTypes["INCREMENT"] = "INCREMENT"; +})(ActionTypes || (ActionTypes = {})); +function testOriginalIssue(action) { + switch (action.type) { + case ActionTypes.INCREMENT: + return 1; + } + // This was the original issue - action should be never but wasn't + var n = action; +} diff --git a/tests/baselines/reference/exhaustiveChecksForNonUnionTypes.symbols b/tests/baselines/reference/exhaustiveChecksForNonUnionTypes.symbols new file mode 100644 index 0000000000000..bff1ca23ec84b --- /dev/null +++ b/tests/baselines/reference/exhaustiveChecksForNonUnionTypes.symbols @@ -0,0 +1,181 @@ +//// [tests/cases/compiler/exhaustiveChecksForNonUnionTypes.ts] //// + +=== exhaustiveChecksForNonUnionTypes.ts === +// Basic case: narrowing non-union types to never +function testBasicNarrowing(obj: { name: "bob" }) { +>testBasicNarrowing : Symbol(testBasicNarrowing, Decl(exhaustiveChecksForNonUnionTypes.ts, 0, 0)) +>obj : Symbol(obj, Decl(exhaustiveChecksForNonUnionTypes.ts, 1, 28)) +>name : Symbol(name, Decl(exhaustiveChecksForNonUnionTypes.ts, 1, 34)) + + if (obj.name === "bob") { +>obj.name : Symbol(name, Decl(exhaustiveChecksForNonUnionTypes.ts, 1, 34)) +>obj : Symbol(obj, Decl(exhaustiveChecksForNonUnionTypes.ts, 1, 28)) +>name : Symbol(name, Decl(exhaustiveChecksForNonUnionTypes.ts, 1, 34)) + + // obj.name is "bob" + } else { + // obj should be narrowed to never since { name: "bob" } with name !== "bob" is impossible + const n: never = obj; +>n : Symbol(n, Decl(exhaustiveChecksForNonUnionTypes.ts, 6, 9)) +>obj : Symbol(obj, Decl(exhaustiveChecksForNonUnionTypes.ts, 1, 28)) + } +} + +// Single enum member case +enum SingleAction { +>SingleAction : Symbol(SingleAction, Decl(exhaustiveChecksForNonUnionTypes.ts, 8, 1)) + + INCREMENT = 'INCREMENT' +>INCREMENT : Symbol(SingleAction.INCREMENT, Decl(exhaustiveChecksForNonUnionTypes.ts, 11, 19)) +} + +interface IIncrement { +>IIncrement : Symbol(IIncrement, Decl(exhaustiveChecksForNonUnionTypes.ts, 13, 1)) + + payload: {}; +>payload : Symbol(IIncrement.payload, Decl(exhaustiveChecksForNonUnionTypes.ts, 15, 22)) + + type: SingleAction.INCREMENT; +>type : Symbol(IIncrement.type, Decl(exhaustiveChecksForNonUnionTypes.ts, 16, 14)) +>SingleAction : Symbol(SingleAction, Decl(exhaustiveChecksForNonUnionTypes.ts, 8, 1)) +>INCREMENT : Symbol(SingleAction.INCREMENT, Decl(exhaustiveChecksForNonUnionTypes.ts, 11, 19)) +} + +function testSingleEnumSwitch(action: IIncrement) { +>testSingleEnumSwitch : Symbol(testSingleEnumSwitch, Decl(exhaustiveChecksForNonUnionTypes.ts, 18, 1)) +>action : Symbol(action, Decl(exhaustiveChecksForNonUnionTypes.ts, 20, 30)) +>IIncrement : Symbol(IIncrement, Decl(exhaustiveChecksForNonUnionTypes.ts, 13, 1)) + + switch (action.type) { +>action.type : Symbol(IIncrement.type, Decl(exhaustiveChecksForNonUnionTypes.ts, 16, 14)) +>action : Symbol(action, Decl(exhaustiveChecksForNonUnionTypes.ts, 20, 30)) +>type : Symbol(IIncrement.type, Decl(exhaustiveChecksForNonUnionTypes.ts, 16, 14)) + + case SingleAction.INCREMENT: +>SingleAction.INCREMENT : Symbol(SingleAction.INCREMENT, Decl(exhaustiveChecksForNonUnionTypes.ts, 11, 19)) +>SingleAction : Symbol(SingleAction, Decl(exhaustiveChecksForNonUnionTypes.ts, 8, 1)) +>INCREMENT : Symbol(SingleAction.INCREMENT, Decl(exhaustiveChecksForNonUnionTypes.ts, 11, 19)) + + return 1; + } + + // action should be narrowed to never since all cases are handled + const n: never = action; +>n : Symbol(n, Decl(exhaustiveChecksForNonUnionTypes.ts, 27, 7)) +>action : Symbol(action, Decl(exhaustiveChecksForNonUnionTypes.ts, 20, 30)) +} + +// Single literal type case (should already work) +function testSingleLiteral(x: "a") { +>testSingleLiteral : Symbol(testSingleLiteral, Decl(exhaustiveChecksForNonUnionTypes.ts, 28, 1)) +>x : Symbol(x, Decl(exhaustiveChecksForNonUnionTypes.ts, 31, 27)) + + if (x === "a") { +>x : Symbol(x, Decl(exhaustiveChecksForNonUnionTypes.ts, 31, 27)) + + // x is "a" + } else { + // x should be never + const n: never = x; +>n : Symbol(n, Decl(exhaustiveChecksForNonUnionTypes.ts, 36, 9)) +>x : Symbol(x, Decl(exhaustiveChecksForNonUnionTypes.ts, 31, 27)) + } +} + +// Single enum value case +enum Single { A = "a" } +>Single : Symbol(Single, Decl(exhaustiveChecksForNonUnionTypes.ts, 38, 1)) +>A : Symbol(Single.A, Decl(exhaustiveChecksForNonUnionTypes.ts, 41, 13)) + +function testSingleEnum(x: Single) { +>testSingleEnum : Symbol(testSingleEnum, Decl(exhaustiveChecksForNonUnionTypes.ts, 41, 23)) +>x : Symbol(x, Decl(exhaustiveChecksForNonUnionTypes.ts, 43, 24)) +>Single : Symbol(Single, Decl(exhaustiveChecksForNonUnionTypes.ts, 38, 1)) + + if (x === Single.A) { +>x : Symbol(x, Decl(exhaustiveChecksForNonUnionTypes.ts, 43, 24)) +>Single.A : Symbol(Single.A, Decl(exhaustiveChecksForNonUnionTypes.ts, 41, 13)) +>Single : Symbol(Single, Decl(exhaustiveChecksForNonUnionTypes.ts, 38, 1)) +>A : Symbol(Single.A, Decl(exhaustiveChecksForNonUnionTypes.ts, 41, 13)) + + // x is Single.A + } else { + // x should be never + const n: never = x; +>n : Symbol(n, Decl(exhaustiveChecksForNonUnionTypes.ts, 48, 9)) +>x : Symbol(x, Decl(exhaustiveChecksForNonUnionTypes.ts, 43, 24)) + } +} + +// More complex object with multiple literal properties +function testComplexObject(obj: { type: "user", status: "active" }) { +>testComplexObject : Symbol(testComplexObject, Decl(exhaustiveChecksForNonUnionTypes.ts, 50, 1)) +>obj : Symbol(obj, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 27)) +>type : Symbol(type, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 33)) +>status : Symbol(status, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 47)) + + if (obj.type === "user") { +>obj.type : Symbol(type, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 33)) +>obj : Symbol(obj, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 27)) +>type : Symbol(type, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 33)) + + if (obj.status === "active") { +>obj.status : Symbol(status, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 47)) +>obj : Symbol(obj, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 27)) +>status : Symbol(status, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 47)) + + // Both properties match + } else { + // obj.status !== "active" but obj: { type: "user", status: "active" } - impossible + const n: never = obj; +>n : Symbol(n, Decl(exhaustiveChecksForNonUnionTypes.ts, 59, 11)) +>obj : Symbol(obj, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 27)) + } + } else { + // obj.type !== "user" but obj: { type: "user", status: "active" } - impossible + const n: never = obj; +>n : Symbol(n, Decl(exhaustiveChecksForNonUnionTypes.ts, 63, 9)) +>obj : Symbol(obj, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 27)) + } +} + +// Switch statement with single case (original issue) +enum ActionTypes { +>ActionTypes : Symbol(ActionTypes, Decl(exhaustiveChecksForNonUnionTypes.ts, 65, 1)) + + INCREMENT = 'INCREMENT', +>INCREMENT : Symbol(ActionTypes.INCREMENT, Decl(exhaustiveChecksForNonUnionTypes.ts, 68, 18)) +} + +interface IAction { +>IAction : Symbol(IAction, Decl(exhaustiveChecksForNonUnionTypes.ts, 70, 1)) + + type: ActionTypes.INCREMENT; +>type : Symbol(IAction.type, Decl(exhaustiveChecksForNonUnionTypes.ts, 72, 19)) +>ActionTypes : Symbol(ActionTypes, Decl(exhaustiveChecksForNonUnionTypes.ts, 65, 1)) +>INCREMENT : Symbol(ActionTypes.INCREMENT, Decl(exhaustiveChecksForNonUnionTypes.ts, 68, 18)) +} + +function testOriginalIssue(action: IAction) { +>testOriginalIssue : Symbol(testOriginalIssue, Decl(exhaustiveChecksForNonUnionTypes.ts, 74, 1)) +>action : Symbol(action, Decl(exhaustiveChecksForNonUnionTypes.ts, 76, 27)) +>IAction : Symbol(IAction, Decl(exhaustiveChecksForNonUnionTypes.ts, 70, 1)) + + switch (action.type) { +>action.type : Symbol(IAction.type, Decl(exhaustiveChecksForNonUnionTypes.ts, 72, 19)) +>action : Symbol(action, Decl(exhaustiveChecksForNonUnionTypes.ts, 76, 27)) +>type : Symbol(IAction.type, Decl(exhaustiveChecksForNonUnionTypes.ts, 72, 19)) + + case ActionTypes.INCREMENT: +>ActionTypes.INCREMENT : Symbol(ActionTypes.INCREMENT, Decl(exhaustiveChecksForNonUnionTypes.ts, 68, 18)) +>ActionTypes : Symbol(ActionTypes, Decl(exhaustiveChecksForNonUnionTypes.ts, 65, 1)) +>INCREMENT : Symbol(ActionTypes.INCREMENT, Decl(exhaustiveChecksForNonUnionTypes.ts, 68, 18)) + + return 1; + } + + // This was the original issue - action should be never but wasn't + const n: never = action; +>n : Symbol(n, Decl(exhaustiveChecksForNonUnionTypes.ts, 83, 7)) +>action : Symbol(action, Decl(exhaustiveChecksForNonUnionTypes.ts, 76, 27)) +} diff --git a/tests/baselines/reference/exhaustiveChecksForNonUnionTypes.types b/tests/baselines/reference/exhaustiveChecksForNonUnionTypes.types new file mode 100644 index 0000000000000..573841765fb5f --- /dev/null +++ b/tests/baselines/reference/exhaustiveChecksForNonUnionTypes.types @@ -0,0 +1,266 @@ +//// [tests/cases/compiler/exhaustiveChecksForNonUnionTypes.ts] //// + +=== exhaustiveChecksForNonUnionTypes.ts === +// Basic case: narrowing non-union types to never +function testBasicNarrowing(obj: { name: "bob" }) { +>testBasicNarrowing : (obj: { name: "bob"; }) => void +> : ^ ^^ ^^^^^^^^^ +>obj : { name: "bob"; } +> : ^^^^^^^^ ^^^ +>name : "bob" +> : ^^^^^ + + if (obj.name === "bob") { +>obj.name === "bob" : boolean +> : ^^^^^^^ +>obj.name : "bob" +> : ^^^^^ +>obj : { name: "bob"; } +> : ^^^^^^^^ ^^^ +>name : "bob" +> : ^^^^^ +>"bob" : "bob" +> : ^^^^^ + + // obj.name is "bob" + } else { + // obj should be narrowed to never since { name: "bob" } with name !== "bob" is impossible + const n: never = obj; +>n : never +> : ^^^^^ +>obj : never +> : ^^^^^ + } +} + +// Single enum member case +enum SingleAction { +>SingleAction : SingleAction +> : ^^^^^^^^^^^^ + + INCREMENT = 'INCREMENT' +>INCREMENT : SingleAction.INCREMENT +> : ^^^^^^^^^^^^^^^^^^^^^^ +>'INCREMENT' : "INCREMENT" +> : ^^^^^^^^^^^ +} + +interface IIncrement { + payload: {}; +>payload : {} +> : ^^ + + type: SingleAction.INCREMENT; +>type : SingleAction +> : ^^^^^^^^^^^^ +>SingleAction : any +> : ^^^ +} + +function testSingleEnumSwitch(action: IIncrement) { +>testSingleEnumSwitch : (action: IIncrement) => number +> : ^ ^^ ^^^^^^^^^^^ +>action : IIncrement +> : ^^^^^^^^^^ + + switch (action.type) { +>action.type : SingleAction +> : ^^^^^^^^^^^^ +>action : IIncrement +> : ^^^^^^^^^^ +>type : SingleAction +> : ^^^^^^^^^^^^ + + case SingleAction.INCREMENT: +>SingleAction.INCREMENT : SingleAction +> : ^^^^^^^^^^^^ +>SingleAction : typeof SingleAction +> : ^^^^^^^^^^^^^^^^^^^ +>INCREMENT : SingleAction +> : ^^^^^^^^^^^^ + + return 1; +>1 : 1 +> : ^ + } + + // action should be narrowed to never since all cases are handled + const n: never = action; +>n : never +> : ^^^^^ +>action : never +> : ^^^^^ +} + +// Single literal type case (should already work) +function testSingleLiteral(x: "a") { +>testSingleLiteral : (x: "a") => void +> : ^ ^^ ^^^^^^^^^ +>x : "a" +> : ^^^ + + if (x === "a") { +>x === "a" : boolean +> : ^^^^^^^ +>x : "a" +> : ^^^ +>"a" : "a" +> : ^^^ + + // x is "a" + } else { + // x should be never + const n: never = x; +>n : never +> : ^^^^^ +>x : never +> : ^^^^^ + } +} + +// Single enum value case +enum Single { A = "a" } +>Single : Single +> : ^^^^^^ +>A : Single.A +> : ^^^^^^^^ +>"a" : "a" +> : ^^^ + +function testSingleEnum(x: Single) { +>testSingleEnum : (x: Single) => void +> : ^ ^^ ^^^^^^^^^ +>x : Single +> : ^^^^^^ + + if (x === Single.A) { +>x === Single.A : boolean +> : ^^^^^^^ +>x : Single +> : ^^^^^^ +>Single.A : Single +> : ^^^^^^ +>Single : typeof Single +> : ^^^^^^^^^^^^^ +>A : Single +> : ^^^^^^ + + // x is Single.A + } else { + // x should be never + const n: never = x; +>n : never +> : ^^^^^ +>x : never +> : ^^^^^ + } +} + +// More complex object with multiple literal properties +function testComplexObject(obj: { type: "user", status: "active" }) { +>testComplexObject : (obj: { type: "user"; status: "active"; }) => void +> : ^ ^^ ^^^^^^^^^ +>obj : { type: "user"; status: "active"; } +> : ^^^^^^^^ ^^^^^^^^^^ ^^^ +>type : "user" +> : ^^^^^^ +>status : "active" +> : ^^^^^^^^ + + if (obj.type === "user") { +>obj.type === "user" : boolean +> : ^^^^^^^ +>obj.type : "user" +> : ^^^^^^ +>obj : { type: "user"; status: "active"; } +> : ^^^^^^^^ ^^^^^^^^^^ ^^^ +>type : "user" +> : ^^^^^^ +>"user" : "user" +> : ^^^^^^ + + if (obj.status === "active") { +>obj.status === "active" : boolean +> : ^^^^^^^ +>obj.status : "active" +> : ^^^^^^^^ +>obj : { type: "user"; status: "active"; } +> : ^^^^^^^^ ^^^^^^^^^^ ^^^ +>status : "active" +> : ^^^^^^^^ +>"active" : "active" +> : ^^^^^^^^ + + // Both properties match + } else { + // obj.status !== "active" but obj: { type: "user", status: "active" } - impossible + const n: never = obj; +>n : never +> : ^^^^^ +>obj : never +> : ^^^^^ + } + } else { + // obj.type !== "user" but obj: { type: "user", status: "active" } - impossible + const n: never = obj; +>n : never +> : ^^^^^ +>obj : never +> : ^^^^^ + } +} + +// Switch statement with single case (original issue) +enum ActionTypes { +>ActionTypes : ActionTypes +> : ^^^^^^^^^^^ + + INCREMENT = 'INCREMENT', +>INCREMENT : ActionTypes.INCREMENT +> : ^^^^^^^^^^^^^^^^^^^^^ +>'INCREMENT' : "INCREMENT" +> : ^^^^^^^^^^^ +} + +interface IAction { + type: ActionTypes.INCREMENT; +>type : ActionTypes +> : ^^^^^^^^^^^ +>ActionTypes : any +> : ^^^ +} + +function testOriginalIssue(action: IAction) { +>testOriginalIssue : (action: IAction) => number +> : ^ ^^ ^^^^^^^^^^^ +>action : IAction +> : ^^^^^^^ + + switch (action.type) { +>action.type : ActionTypes +> : ^^^^^^^^^^^ +>action : IAction +> : ^^^^^^^ +>type : ActionTypes +> : ^^^^^^^^^^^ + + case ActionTypes.INCREMENT: +>ActionTypes.INCREMENT : ActionTypes +> : ^^^^^^^^^^^ +>ActionTypes : typeof ActionTypes +> : ^^^^^^^^^^^^^^^^^^ +>INCREMENT : ActionTypes +> : ^^^^^^^^^^^ + + return 1; +>1 : 1 +> : ^ + } + + // This was the original issue - action should be never but wasn't + const n: never = action; +>n : never +> : ^^^^^ +>action : never +> : ^^^^^ +} diff --git a/tests/cases/compiler/exhaustiveChecksForNonUnionTypes.ts b/tests/cases/compiler/exhaustiveChecksForNonUnionTypes.ts new file mode 100644 index 0000000000000..1b2f551a5cd51 --- /dev/null +++ b/tests/cases/compiler/exhaustiveChecksForNonUnionTypes.ts @@ -0,0 +1,87 @@ +// @strict: true + +// Basic case: narrowing non-union types to never +function testBasicNarrowing(obj: { name: "bob" }) { + if (obj.name === "bob") { + // obj.name is "bob" + } else { + // obj should be narrowed to never since { name: "bob" } with name !== "bob" is impossible + const n: never = obj; + } +} + +// Single enum member case +enum SingleAction { + INCREMENT = 'INCREMENT' +} + +interface IIncrement { + payload: {}; + type: SingleAction.INCREMENT; +} + +function testSingleEnumSwitch(action: IIncrement) { + switch (action.type) { + case SingleAction.INCREMENT: + return 1; + } + + // action should be narrowed to never since all cases are handled + const n: never = action; +} + +// Single literal type case (should already work) +function testSingleLiteral(x: "a") { + if (x === "a") { + // x is "a" + } else { + // x should be never + const n: never = x; + } +} + +// Single enum value case +enum Single { A = "a" } + +function testSingleEnum(x: Single) { + if (x === Single.A) { + // x is Single.A + } else { + // x should be never + const n: never = x; + } +} + +// More complex object with multiple literal properties +function testComplexObject(obj: { type: "user", status: "active" }) { + if (obj.type === "user") { + if (obj.status === "active") { + // Both properties match + } else { + // obj.status !== "active" but obj: { type: "user", status: "active" } - impossible + const n: never = obj; + } + } else { + // obj.type !== "user" but obj: { type: "user", status: "active" } - impossible + const n: never = obj; + } +} + +// Switch statement with single case (original issue) +enum ActionTypes { + INCREMENT = 'INCREMENT', +} + +interface IAction { + type: ActionTypes.INCREMENT; +} + +function testOriginalIssue(action: IAction) { + switch (action.type) { + case ActionTypes.INCREMENT: + return 1; + } + + // This was the original issue - action should be never but wasn't + const n: never = action; +} \ No newline at end of file From 7f0a0e542bf3d489ddda2c7ccafc87be04d221a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 23 Jul 2025 21:10:06 +0000 Subject: [PATCH 3/4] Apply formatting fixes --- src/compiler/checker.ts | 234 ++++++++++++++++++++-------------------- 1 file changed, 117 insertions(+), 117 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 7c28a2c6045d5..333f0ccbcfdbb 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -29380,59 +29380,59 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return undefined; } - function getDiscriminantPropertyAccess(expr: Expression, computedType: Type) { - // As long as the computed type is a subset of the declared type, we use the full declared type to detect - // a discriminant property. In cases where the computed type isn't a subset, e.g because of a preceding type - // predicate narrowing, we use the actual computed type. - if (declaredType.flags & TypeFlags.Union || computedType.flags & TypeFlags.Union) { - const access = getCandidateDiscriminantPropertyAccess(expr); - if (access) { - const name = getAccessedPropertyName(access); - if (name) { - const type = declaredType.flags & TypeFlags.Union && isTypeSubsetOf(computedType, declaredType) ? declaredType : computedType; - if (isDiscriminantProperty(type, name)) { - return access; - } - } - } - } - // Fix for #23572: Allow discriminant property narrowing for non-union types - // This enables narrowing to never when all possibilities are eliminated - else { - const access = getCandidateDiscriminantPropertyAccess(expr); - if (access) { - const name = getAccessedPropertyName(access); - if (name) { - // For non-union types, check if the property exists and has a literal type - const type = declaredType.flags & TypeFlags.Union && isTypeSubsetOf(computedType, declaredType) ? declaredType : computedType; - const propType = getTypeOfPropertyOfType(type, name); - if (propType && isUnitLikeType(propType)) { - return access; - } - } - } - } - return undefined; - } - - function narrowTypeByDiscriminant(type: Type, access: AccessExpression | BindingElement | ParameterDeclaration, narrowType: (t: Type) => Type): Type { - const propName = getAccessedPropertyName(access); - if (propName === undefined) { - return type; - } - const optionalChain = isOptionalChain(access); - const removeNullable = strictNullChecks && (optionalChain || isNonNullAccess(access)) && maybeTypeOfKind(type, TypeFlags.Nullable); - let propType = getTypeOfPropertyOfType(removeNullable ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type, propName); - if (!propType) { - return type; - } - propType = removeNullable && optionalChain ? getOptionalType(propType) : propType; - const narrowedPropType = narrowType(propType); - return filterType(type, t => { - const discriminantType = getTypeOfPropertyOrIndexSignatureOfType(t, propName) || unknownType; - const result = !(discriminantType.flags & TypeFlags.Never) && !(narrowedPropType.flags & TypeFlags.Never) && areTypesComparable(narrowedPropType, discriminantType); - return result; - }); + function getDiscriminantPropertyAccess(expr: Expression, computedType: Type) { + // As long as the computed type is a subset of the declared type, we use the full declared type to detect + // a discriminant property. In cases where the computed type isn't a subset, e.g because of a preceding type + // predicate narrowing, we use the actual computed type. + if (declaredType.flags & TypeFlags.Union || computedType.flags & TypeFlags.Union) { + const access = getCandidateDiscriminantPropertyAccess(expr); + if (access) { + const name = getAccessedPropertyName(access); + if (name) { + const type = declaredType.flags & TypeFlags.Union && isTypeSubsetOf(computedType, declaredType) ? declaredType : computedType; + if (isDiscriminantProperty(type, name)) { + return access; + } + } + } + } + // Fix for #23572: Allow discriminant property narrowing for non-union types + // This enables narrowing to never when all possibilities are eliminated + else { + const access = getCandidateDiscriminantPropertyAccess(expr); + if (access) { + const name = getAccessedPropertyName(access); + if (name) { + // For non-union types, check if the property exists and has a literal type + const type = declaredType.flags & TypeFlags.Union && isTypeSubsetOf(computedType, declaredType) ? declaredType : computedType; + const propType = getTypeOfPropertyOfType(type, name); + if (propType && isUnitLikeType(propType)) { + return access; + } + } + } + } + return undefined; + } + + function narrowTypeByDiscriminant(type: Type, access: AccessExpression | BindingElement | ParameterDeclaration, narrowType: (t: Type) => Type): Type { + const propName = getAccessedPropertyName(access); + if (propName === undefined) { + return type; + } + const optionalChain = isOptionalChain(access); + const removeNullable = strictNullChecks && (optionalChain || isNonNullAccess(access)) && maybeTypeOfKind(type, TypeFlags.Nullable); + let propType = getTypeOfPropertyOfType(removeNullable ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type, propName); + if (!propType) { + return type; + } + propType = removeNullable && optionalChain ? getOptionalType(propType) : propType; + const narrowedPropType = narrowType(propType); + return filterType(type, t => { + const discriminantType = getTypeOfPropertyOrIndexSignatureOfType(t, propName) || unknownType; + const result = !(discriminantType.flags & TypeFlags.Never) && !(narrowedPropType.flags & TypeFlags.Never) && areTypesComparable(narrowedPropType, discriminantType); + return result; + }); } function narrowTypeByDiscriminantProperty(type: Type, access: AccessExpression | BindingElement | ParameterDeclaration, operator: SyntaxKind, value: Expression, assumeTrue: boolean) { @@ -29635,43 +29635,43 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return removeNullable ? getAdjustedTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type; } - function narrowTypeByEquality(type: Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): Type { - if (type.flags & TypeFlags.Any) { - return type; - } - if (operator === SyntaxKind.ExclamationEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) { - assumeTrue = !assumeTrue; - } - const valueType = getTypeOfExpression(value); - const doubleEquals = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken; - if (valueType.flags & TypeFlags.Nullable) { - if (!strictNullChecks) { - return type; - } - const facts = doubleEquals ? - assumeTrue ? TypeFacts.EQUndefinedOrNull : TypeFacts.NEUndefinedOrNull : - valueType.flags & TypeFlags.Null ? - assumeTrue ? TypeFacts.EQNull : TypeFacts.NENull : - assumeTrue ? TypeFacts.EQUndefined : TypeFacts.NEUndefined; - return getAdjustedTypeWithFacts(type, facts); - } - if (assumeTrue) { - if (!doubleEquals && (type.flags & TypeFlags.Unknown || someType(type, isEmptyAnonymousObjectType))) { - if (valueType.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive) || isEmptyAnonymousObjectType(valueType)) { - return valueType; - } - if (valueType.flags & TypeFlags.Object) { - return nonPrimitiveType; - } - } - const filteredType = filterType(type, t => areTypesComparable(t, valueType) || doubleEquals && isCoercibleUnderDoubleEquals(t, valueType)); - return replacePrimitivesWithLiterals(filteredType, valueType); - } - if (isUnitType(valueType)) { - const result = filterType(type, t => !(isUnitLikeType(t) && areTypesComparable(t, valueType))); - return result; - } - return type; + function narrowTypeByEquality(type: Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): Type { + if (type.flags & TypeFlags.Any) { + return type; + } + if (operator === SyntaxKind.ExclamationEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) { + assumeTrue = !assumeTrue; + } + const valueType = getTypeOfExpression(value); + const doubleEquals = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken; + if (valueType.flags & TypeFlags.Nullable) { + if (!strictNullChecks) { + return type; + } + const facts = doubleEquals ? + assumeTrue ? TypeFacts.EQUndefinedOrNull : TypeFacts.NEUndefinedOrNull : + valueType.flags & TypeFlags.Null ? + assumeTrue ? TypeFacts.EQNull : TypeFacts.NENull : + assumeTrue ? TypeFacts.EQUndefined : TypeFacts.NEUndefined; + return getAdjustedTypeWithFacts(type, facts); + } + if (assumeTrue) { + if (!doubleEquals && (type.flags & TypeFlags.Unknown || someType(type, isEmptyAnonymousObjectType))) { + if (valueType.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive) || isEmptyAnonymousObjectType(valueType)) { + return valueType; + } + if (valueType.flags & TypeFlags.Object) { + return nonPrimitiveType; + } + } + const filteredType = filterType(type, t => areTypesComparable(t, valueType) || doubleEquals && isCoercibleUnderDoubleEquals(t, valueType)); + return replacePrimitivesWithLiterals(filteredType, valueType); + } + if (isUnitType(valueType)) { + const result = filterType(type, t => !(isUnitLikeType(t) && areTypesComparable(t, valueType))); + return result; + } + return type; } function narrowTypeByTypeof(type: Type, typeOfExpr: TypeOfExpression, operator: SyntaxKind, literal: LiteralExpression, assumeTrue: boolean): Type { @@ -39268,33 +39268,33 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return links.isExhaustive; } - function computeExhaustiveSwitchStatement(node: SwitchStatement): boolean { - if (node.expression.kind === SyntaxKind.TypeOfExpression) { - const witnesses = getSwitchClauseTypeOfWitnesses(node); - if (!witnesses) { - return false; - } - const operandConstraint = getBaseConstraintOrType(checkExpressionCached((node.expression as TypeOfExpression).expression)); - // Get the not-equal flags for all handled cases. - const notEqualFacts = getNotEqualFactsFromTypeofSwitch(0, 0, witnesses); - if (operandConstraint.flags & TypeFlags.AnyOrUnknown) { - // We special case the top types to be exhaustive when all cases are handled. - return (TypeFacts.AllTypeofNE & notEqualFacts) === TypeFacts.AllTypeofNE; - } - // A missing not-equal flag indicates that the type wasn't handled by some case. - return !someType(operandConstraint, t => getTypeFacts(t, notEqualFacts) === notEqualFacts); - } - const type = getBaseConstraintOrType(checkExpressionCached(node.expression)); - if (!isLiteralType(type)) { - return false; - } - const switchTypes = getSwitchClauseTypes(node); - if (!switchTypes.length || some(switchTypes, isNeitherUnitTypeNorNever)) { - return false; - } - const mappedType = mapType(type, getRegularTypeOfLiteralType); - const result = eachTypeContainedIn(mappedType, switchTypes); - return result; + function computeExhaustiveSwitchStatement(node: SwitchStatement): boolean { + if (node.expression.kind === SyntaxKind.TypeOfExpression) { + const witnesses = getSwitchClauseTypeOfWitnesses(node); + if (!witnesses) { + return false; + } + const operandConstraint = getBaseConstraintOrType(checkExpressionCached((node.expression as TypeOfExpression).expression)); + // Get the not-equal flags for all handled cases. + const notEqualFacts = getNotEqualFactsFromTypeofSwitch(0, 0, witnesses); + if (operandConstraint.flags & TypeFlags.AnyOrUnknown) { + // We special case the top types to be exhaustive when all cases are handled. + return (TypeFacts.AllTypeofNE & notEqualFacts) === TypeFacts.AllTypeofNE; + } + // A missing not-equal flag indicates that the type wasn't handled by some case. + return !someType(operandConstraint, t => getTypeFacts(t, notEqualFacts) === notEqualFacts); + } + const type = getBaseConstraintOrType(checkExpressionCached(node.expression)); + if (!isLiteralType(type)) { + return false; + } + const switchTypes = getSwitchClauseTypes(node); + if (!switchTypes.length || some(switchTypes, isNeitherUnitTypeNorNever)) { + return false; + } + const mappedType = mapType(type, getRegularTypeOfLiteralType); + const result = eachTypeContainedIn(mappedType, switchTypes); + return result; } function functionHasImplicitReturn(func: FunctionLikeDeclaration) { From f820fc887fd1d9777131c3eff09313ee0bceb2bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 23 Jul 2025 21:50:39 +0000 Subject: [PATCH 4/4] Fix EvolvingArray crash in discriminant property narrowing Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- src/compiler/checker.ts | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 333f0ccbcfdbb..945ae034b25e5 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -29397,20 +29397,23 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } // Fix for #23572: Allow discriminant property narrowing for non-union types - // This enables narrowing to never when all possibilities are eliminated - else { - const access = getCandidateDiscriminantPropertyAccess(expr); - if (access) { - const name = getAccessedPropertyName(access); - if (name) { - // For non-union types, check if the property exists and has a literal type - const type = declaredType.flags & TypeFlags.Union && isTypeSubsetOf(computedType, declaredType) ? declaredType : computedType; - const propType = getTypeOfPropertyOfType(type, name); - if (propType && isUnitLikeType(propType)) { - return access; - } - } - } + // This enables narrowing to never when all possibilities are eliminated + else { + const access = getCandidateDiscriminantPropertyAccess(expr); + if (access) { + const name = getAccessedPropertyName(access); + if (name) { + // For non-union types, check if the property exists and has a literal type + const type = declaredType.flags & TypeFlags.Union && isTypeSubsetOf(computedType, declaredType) ? declaredType : computedType; + // Only try to get property type for safe types (avoid EvolvingArray and other special types) + if (type.flags & TypeFlags.Object && !((type as ObjectType).objectFlags & ObjectFlags.EvolvingArray)) { + const propType = getTypeOfPropertyOfType(type, name); + if (propType && isUnitLikeType(propType)) { + return access; + } + } + } + } } return undefined; }