Fix nullability intersections in CFA and relations (#57724)

This commit is contained in:
Anders Hejlsberg
2024-03-11 14:00:56 -07:00
committed by GitHub
parent e24d886305
commit ef6a4ab5c7
4 changed files with 394 additions and 3 deletions

View File

@@ -21171,7 +21171,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (reduced !== type) {
return reduced;
}
if (type.flags & TypeFlags.Intersection && some((type as IntersectionType).types, isEmptyAnonymousObjectType)) {
if (type.flags & TypeFlags.Intersection && shouldNormalizeIntersection(type as IntersectionType)) {
// Normalization handles cases like
// Partial<T>[K] & ({} | null) ==>
// Partial<T>[K] & {} | Partial<T>[K} & null ==>
// (T[K] | undefined) & {} | (T[K] | undefined) & null ==>
// T[K] & {} | undefined & {} | T[K] & null | undefined & null ==>
// T[K] & {} | T[K] & null
const normalizedTypes = sameMap(type.types, t => getNormalizedType(t, writing));
if (normalizedTypes !== type.types) {
return getIntersectionType(normalizedTypes);
@@ -21180,6 +21186,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return type;
}
function shouldNormalizeIntersection(type: IntersectionType) {
let hasInstantiable = false;
let hasNullableOrEmpty = false;
for (const t of type.types) {
hasInstantiable ||= !!(t.flags & TypeFlags.Instantiable);
hasNullableOrEmpty ||= !!(t.flags & TypeFlags.Nullable) || isEmptyAnonymousObjectType(t);
if (hasInstantiable && hasNullableOrEmpty) return true;
}
return false;
}
function getNormalizedTupleType(type: TupleTypeReference, writing: boolean): Type {
const elements = getElementTypes(type);
const normalizedElements = sameMap(elements, t => t.flags & TypeFlags.Simplifiable ? getSimplifiedType(t, writing) : t);
@@ -26968,9 +26985,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (strictNullChecks) {
switch (facts) {
case TypeFacts.NEUndefined:
return mapType(reduced, t => hasTypeFacts(t, TypeFacts.EQUndefined) ? getIntersectionType([t, hasTypeFacts(t, TypeFacts.EQNull) && !maybeTypeOfKind(reduced, TypeFlags.Null) ? getUnionType([emptyObjectType, nullType]) : emptyObjectType]) : t);
return removeNullableByIntersection(reduced, TypeFacts.EQUndefined, TypeFacts.EQNull, TypeFacts.IsNull, nullType);
case TypeFacts.NENull:
return mapType(reduced, t => hasTypeFacts(t, TypeFacts.EQNull) ? getIntersectionType([t, hasTypeFacts(t, TypeFacts.EQUndefined) && !maybeTypeOfKind(reduced, TypeFlags.Undefined) ? getUnionType([emptyObjectType, undefinedType]) : emptyObjectType]) : t);
return removeNullableByIntersection(reduced, TypeFacts.EQNull, TypeFacts.EQUndefined, TypeFacts.IsUndefined, undefinedType);
case TypeFacts.NEUndefinedOrNull:
case TypeFacts.Truthy:
return mapType(reduced, t => hasTypeFacts(t, TypeFacts.EQUndefinedOrNull) ? getGlobalNonNullableTypeInstantiation(t) : t);
@@ -26979,6 +26996,20 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return reduced;
}
function removeNullableByIntersection(type: Type, targetFacts: TypeFacts, otherFacts: TypeFacts, otherIncludesFacts: TypeFacts, otherType: Type) {
const facts = getTypeFacts(type, TypeFacts.EQUndefined | TypeFacts.EQNull | TypeFacts.IsUndefined | TypeFacts.IsNull);
// Simply return the type if it never compares equal to the target nullable.
if (!(facts & targetFacts)) {
return type;
}
// By default we intersect with a union of {} and the opposite nullable.
const emptyAndOtherUnion = getUnionType([emptyObjectType, otherType]);
// For each constituent type that can compare equal to the target nullable, intersect with the above union
// if the type doesn't already include the opppsite nullable and the constituent can compare equal to the
// opposite nullable; otherwise, just intersect with {}.
return mapType(type, t => hasTypeFacts(t, targetFacts) ? getIntersectionType([t, !(facts & otherIncludesFacts) && hasTypeFacts(t, otherFacts) ? emptyAndOtherUnion : emptyObjectType]) : t);
}
function recombineUnknownType(type: Type) {
return type === unknownUnionType ? unknownType : type;
}