diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 7c28a2c6045..333f0ccbcfd 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 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 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) {