From ef6a4ab5c766f42a6d2b18dbb8d3f20b442ba4c4 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Mon, 11 Mar 2024 14:00:56 -0700 Subject: [PATCH] Fix nullability intersections in CFA and relations (#57724) --- src/compiler/checker.ts | 37 +++- .../indexedAccessAndNullableNarrowing.symbols | 177 ++++++++++++++++++ .../indexedAccessAndNullableNarrowing.types | 136 ++++++++++++++ .../indexedAccessAndNullableNarrowing.ts | 47 +++++ 4 files changed, 394 insertions(+), 3 deletions(-) create mode 100644 tests/baselines/reference/indexedAccessAndNullableNarrowing.symbols create mode 100644 tests/baselines/reference/indexedAccessAndNullableNarrowing.types create mode 100644 tests/cases/compiler/indexedAccessAndNullableNarrowing.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 13dac76261b..e7fac49a145 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -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[K] & ({} | null) ==> + // Partial[K] & {} | Partial[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; } diff --git a/tests/baselines/reference/indexedAccessAndNullableNarrowing.symbols b/tests/baselines/reference/indexedAccessAndNullableNarrowing.symbols new file mode 100644 index 00000000000..85b1f581d2e --- /dev/null +++ b/tests/baselines/reference/indexedAccessAndNullableNarrowing.symbols @@ -0,0 +1,177 @@ +//// [tests/cases/compiler/indexedAccessAndNullableNarrowing.ts] //// + +=== indexedAccessAndNullableNarrowing.ts === +function f1, K extends keyof T>(x: T[K] | undefined) { +>f1 : Symbol(f1, Decl(indexedAccessAndNullableNarrowing.ts, 0, 0)) +>T : Symbol(T, Decl(indexedAccessAndNullableNarrowing.ts, 0, 12)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) +>K : Symbol(K, Decl(indexedAccessAndNullableNarrowing.ts, 0, 42)) +>T : Symbol(T, Decl(indexedAccessAndNullableNarrowing.ts, 0, 12)) +>x : Symbol(x, Decl(indexedAccessAndNullableNarrowing.ts, 0, 62)) +>T : Symbol(T, Decl(indexedAccessAndNullableNarrowing.ts, 0, 12)) +>K : Symbol(K, Decl(indexedAccessAndNullableNarrowing.ts, 0, 42)) + + if (x === undefined) return; +>x : Symbol(x, Decl(indexedAccessAndNullableNarrowing.ts, 0, 62)) +>undefined : Symbol(undefined) + + x; // T[K] & ({} | null) +>x : Symbol(x, Decl(indexedAccessAndNullableNarrowing.ts, 0, 62)) + + if (x === undefined) return; +>x : Symbol(x, Decl(indexedAccessAndNullableNarrowing.ts, 0, 62)) +>undefined : Symbol(undefined) + + x; // T[K] & ({} | null) +>x : Symbol(x, Decl(indexedAccessAndNullableNarrowing.ts, 0, 62)) +} + +function f2, K extends keyof T>(x: T[K] | null) { +>f2 : Symbol(f2, Decl(indexedAccessAndNullableNarrowing.ts, 5, 1)) +>T : Symbol(T, Decl(indexedAccessAndNullableNarrowing.ts, 7, 12)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) +>K : Symbol(K, Decl(indexedAccessAndNullableNarrowing.ts, 7, 42)) +>T : Symbol(T, Decl(indexedAccessAndNullableNarrowing.ts, 7, 12)) +>x : Symbol(x, Decl(indexedAccessAndNullableNarrowing.ts, 7, 62)) +>T : Symbol(T, Decl(indexedAccessAndNullableNarrowing.ts, 7, 12)) +>K : Symbol(K, Decl(indexedAccessAndNullableNarrowing.ts, 7, 42)) + + if (x === null) return; +>x : Symbol(x, Decl(indexedAccessAndNullableNarrowing.ts, 7, 62)) + + x; // T[K] & ({} | undefined) +>x : Symbol(x, Decl(indexedAccessAndNullableNarrowing.ts, 7, 62)) + + if (x === null) return; +>x : Symbol(x, Decl(indexedAccessAndNullableNarrowing.ts, 7, 62)) + + x; // T[K] & ({} | undefined) +>x : Symbol(x, Decl(indexedAccessAndNullableNarrowing.ts, 7, 62)) +} + +function f3(t: T[K], p1: Partial[K] & {}, p2: Partial[K] & ({} | null)) { +>f3 : Symbol(f3, Decl(indexedAccessAndNullableNarrowing.ts, 12, 1)) +>T : Symbol(T, Decl(indexedAccessAndNullableNarrowing.ts, 14, 12)) +>K : Symbol(K, Decl(indexedAccessAndNullableNarrowing.ts, 14, 14)) +>T : Symbol(T, Decl(indexedAccessAndNullableNarrowing.ts, 14, 12)) +>t : Symbol(t, Decl(indexedAccessAndNullableNarrowing.ts, 14, 34)) +>T : Symbol(T, Decl(indexedAccessAndNullableNarrowing.ts, 14, 12)) +>K : Symbol(K, Decl(indexedAccessAndNullableNarrowing.ts, 14, 14)) +>p1 : Symbol(p1, Decl(indexedAccessAndNullableNarrowing.ts, 14, 42)) +>Partial : Symbol(Partial, Decl(lib.es5.d.ts, --, --)) +>T : Symbol(T, Decl(indexedAccessAndNullableNarrowing.ts, 14, 12)) +>K : Symbol(K, Decl(indexedAccessAndNullableNarrowing.ts, 14, 14)) +>p2 : Symbol(p2, Decl(indexedAccessAndNullableNarrowing.ts, 14, 66)) +>Partial : Symbol(Partial, Decl(lib.es5.d.ts, --, --)) +>T : Symbol(T, Decl(indexedAccessAndNullableNarrowing.ts, 14, 12)) +>K : Symbol(K, Decl(indexedAccessAndNullableNarrowing.ts, 14, 14)) + + t = p1; +>t : Symbol(t, Decl(indexedAccessAndNullableNarrowing.ts, 14, 34)) +>p1 : Symbol(p1, Decl(indexedAccessAndNullableNarrowing.ts, 14, 42)) + + t = p2; +>t : Symbol(t, Decl(indexedAccessAndNullableNarrowing.ts, 14, 34)) +>p2 : Symbol(p2, Decl(indexedAccessAndNullableNarrowing.ts, 14, 66)) +} + +// https://github.com/microsoft/TypeScript/issues/57693 + +type AnyObject = Record; +>AnyObject : Symbol(AnyObject, Decl(indexedAccessAndNullableNarrowing.ts, 17, 1)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) + +type State = AnyObject; +>State : Symbol(State, Decl(indexedAccessAndNullableNarrowing.ts, 21, 37)) +>AnyObject : Symbol(AnyObject, Decl(indexedAccessAndNullableNarrowing.ts, 17, 1)) + +declare function hasOwnProperty( +>hasOwnProperty : Symbol(hasOwnProperty, Decl(indexedAccessAndNullableNarrowing.ts, 22, 23)) +>T : Symbol(T, Decl(indexedAccessAndNullableNarrowing.ts, 24, 32)) +>AnyObject : Symbol(AnyObject, Decl(indexedAccessAndNullableNarrowing.ts, 17, 1)) + + object: T, +>object : Symbol(object, Decl(indexedAccessAndNullableNarrowing.ts, 24, 53)) +>T : Symbol(T, Decl(indexedAccessAndNullableNarrowing.ts, 24, 32)) + + prop: PropertyKey, +>prop : Symbol(prop, Decl(indexedAccessAndNullableNarrowing.ts, 25, 14)) +>PropertyKey : Symbol(PropertyKey, Decl(lib.es5.d.ts, --, --)) + +): prop is keyof T; +>prop : Symbol(prop, Decl(indexedAccessAndNullableNarrowing.ts, 25, 14)) +>T : Symbol(T, Decl(indexedAccessAndNullableNarrowing.ts, 24, 32)) + +interface Store { +>Store : Symbol(Store, Decl(indexedAccessAndNullableNarrowing.ts, 27, 19)) +>S : Symbol(S, Decl(indexedAccessAndNullableNarrowing.ts, 29, 16)) +>State : Symbol(State, Decl(indexedAccessAndNullableNarrowing.ts, 21, 37)) + + setState(key: K, value: S[K]): void; +>setState : Symbol(Store.setState, Decl(indexedAccessAndNullableNarrowing.ts, 29, 28)) +>K : Symbol(K, Decl(indexedAccessAndNullableNarrowing.ts, 30, 13)) +>S : Symbol(S, Decl(indexedAccessAndNullableNarrowing.ts, 29, 16)) +>key : Symbol(key, Decl(indexedAccessAndNullableNarrowing.ts, 30, 32)) +>K : Symbol(K, Decl(indexedAccessAndNullableNarrowing.ts, 30, 13)) +>value : Symbol(value, Decl(indexedAccessAndNullableNarrowing.ts, 30, 39)) +>S : Symbol(S, Decl(indexedAccessAndNullableNarrowing.ts, 29, 16)) +>K : Symbol(K, Decl(indexedAccessAndNullableNarrowing.ts, 30, 13)) +} + +export function syncStoreProp< +>syncStoreProp : Symbol(syncStoreProp, Decl(indexedAccessAndNullableNarrowing.ts, 31, 1)) + + S extends State, +>S : Symbol(S, Decl(indexedAccessAndNullableNarrowing.ts, 33, 30)) +>State : Symbol(State, Decl(indexedAccessAndNullableNarrowing.ts, 21, 37)) + + P extends Partial, +>P : Symbol(P, Decl(indexedAccessAndNullableNarrowing.ts, 34, 20)) +>Partial : Symbol(Partial, Decl(lib.es5.d.ts, --, --)) +>S : Symbol(S, Decl(indexedAccessAndNullableNarrowing.ts, 33, 30)) + + K extends keyof S, +>K : Symbol(K, Decl(indexedAccessAndNullableNarrowing.ts, 35, 25)) +>S : Symbol(S, Decl(indexedAccessAndNullableNarrowing.ts, 33, 30)) + +>(store: Store, props: P, key: K) { +>store : Symbol(store, Decl(indexedAccessAndNullableNarrowing.ts, 37, 2)) +>Store : Symbol(Store, Decl(indexedAccessAndNullableNarrowing.ts, 27, 19)) +>S : Symbol(S, Decl(indexedAccessAndNullableNarrowing.ts, 33, 30)) +>props : Symbol(props, Decl(indexedAccessAndNullableNarrowing.ts, 37, 18)) +>P : Symbol(P, Decl(indexedAccessAndNullableNarrowing.ts, 34, 20)) +>key : Symbol(key, Decl(indexedAccessAndNullableNarrowing.ts, 37, 28)) +>K : Symbol(K, Decl(indexedAccessAndNullableNarrowing.ts, 35, 25)) + + const value = hasOwnProperty(props, key) ? props[key] : undefined; +>value : Symbol(value, Decl(indexedAccessAndNullableNarrowing.ts, 38, 9)) +>hasOwnProperty : Symbol(hasOwnProperty, Decl(indexedAccessAndNullableNarrowing.ts, 22, 23)) +>props : Symbol(props, Decl(indexedAccessAndNullableNarrowing.ts, 37, 18)) +>key : Symbol(key, Decl(indexedAccessAndNullableNarrowing.ts, 37, 28)) +>props : Symbol(props, Decl(indexedAccessAndNullableNarrowing.ts, 37, 18)) +>key : Symbol(key, Decl(indexedAccessAndNullableNarrowing.ts, 37, 28)) +>undefined : Symbol(undefined) + + if (value === undefined) return; +>value : Symbol(value, Decl(indexedAccessAndNullableNarrowing.ts, 38, 9)) +>undefined : Symbol(undefined) + + store.setState(key, value); +>store.setState : Symbol(Store.setState, Decl(indexedAccessAndNullableNarrowing.ts, 29, 28)) +>store : Symbol(store, Decl(indexedAccessAndNullableNarrowing.ts, 37, 2)) +>setState : Symbol(Store.setState, Decl(indexedAccessAndNullableNarrowing.ts, 29, 28)) +>key : Symbol(key, Decl(indexedAccessAndNullableNarrowing.ts, 37, 28)) +>value : Symbol(value, Decl(indexedAccessAndNullableNarrowing.ts, 38, 9)) + + if (value === undefined) return; +>value : Symbol(value, Decl(indexedAccessAndNullableNarrowing.ts, 38, 9)) +>undefined : Symbol(undefined) + + store.setState(key, value); +>store.setState : Symbol(Store.setState, Decl(indexedAccessAndNullableNarrowing.ts, 29, 28)) +>store : Symbol(store, Decl(indexedAccessAndNullableNarrowing.ts, 37, 2)) +>setState : Symbol(Store.setState, Decl(indexedAccessAndNullableNarrowing.ts, 29, 28)) +>key : Symbol(key, Decl(indexedAccessAndNullableNarrowing.ts, 37, 28)) +>value : Symbol(value, Decl(indexedAccessAndNullableNarrowing.ts, 38, 9)) +} + diff --git a/tests/baselines/reference/indexedAccessAndNullableNarrowing.types b/tests/baselines/reference/indexedAccessAndNullableNarrowing.types new file mode 100644 index 00000000000..6385f1ceb40 --- /dev/null +++ b/tests/baselines/reference/indexedAccessAndNullableNarrowing.types @@ -0,0 +1,136 @@ +//// [tests/cases/compiler/indexedAccessAndNullableNarrowing.ts] //// + +=== indexedAccessAndNullableNarrowing.ts === +function f1, K extends keyof T>(x: T[K] | undefined) { +>f1 : , K extends keyof T>(x: T[K] | undefined) => void +>x : T[K] | undefined + + if (x === undefined) return; +>x === undefined : boolean +>x : T[K] | undefined +>undefined : undefined + + x; // T[K] & ({} | null) +>x : T[K] & ({} | null) + + if (x === undefined) return; +>x === undefined : boolean +>x : T[K] & ({} | null) +>undefined : undefined + + x; // T[K] & ({} | null) +>x : T[K] & ({} | null) +} + +function f2, K extends keyof T>(x: T[K] | null) { +>f2 : , K extends keyof T>(x: T[K] | null) => void +>x : T[K] | null + + if (x === null) return; +>x === null : boolean +>x : T[K] | null + + x; // T[K] & ({} | undefined) +>x : T[K] & ({} | undefined) + + if (x === null) return; +>x === null : boolean +>x : T[K] & ({} | undefined) + + x; // T[K] & ({} | undefined) +>x : T[K] & ({} | undefined) +} + +function f3(t: T[K], p1: Partial[K] & {}, p2: Partial[K] & ({} | null)) { +>f3 : (t: T[K], p1: Partial[K] & {}, p2: Partial[K] & ({} | null)) => void +>t : T[K] +>p1 : Partial[K] & {} +>p2 : Partial[K] & ({} | null) + + t = p1; +>t = p1 : Partial[K] & {} +>t : T[K] +>p1 : Partial[K] & {} + + t = p2; +>t = p2 : Partial[K] & ({} | null) +>t : T[K] +>p2 : Partial[K] & ({} | null) +} + +// https://github.com/microsoft/TypeScript/issues/57693 + +type AnyObject = Record; +>AnyObject : { [x: string]: any; } + +type State = AnyObject; +>State : AnyObject + +declare function hasOwnProperty( +>hasOwnProperty : (object: T, prop: PropertyKey) => prop is keyof T + + object: T, +>object : T + + prop: PropertyKey, +>prop : PropertyKey + +): prop is keyof T; + +interface Store { + setState(key: K, value: S[K]): void; +>setState : (key: K, value: S[K]) => void +>key : K +>value : S[K] +} + +export function syncStoreProp< +>syncStoreProp : , K extends keyof S>(store: Store, props: P, key: K) => void + + S extends State, + P extends Partial, + K extends keyof S, +>(store: Store, props: P, key: K) { +>store : Store +>props : P +>key : K + + const value = hasOwnProperty(props, key) ? props[key] : undefined; +>value : P[K] | undefined +>hasOwnProperty(props, key) ? props[key] : undefined : P[K] | undefined +>hasOwnProperty(props, key) : boolean +>hasOwnProperty : (object: T, prop: PropertyKey) => prop is keyof T +>props : P +>key : string | number | symbol +>props[key] : P[K] +>props : P +>key : K +>undefined : undefined + + if (value === undefined) return; +>value === undefined : boolean +>value : P[K] | undefined +>undefined : undefined + + store.setState(key, value); +>store.setState(key, value) : void +>store.setState : (key: K_1, value: S[K_1]) => void +>store : Store +>setState : (key: K_1, value: S[K_1]) => void +>key : K +>value : P[K] & ({} | null) + + if (value === undefined) return; +>value === undefined : boolean +>value : P[K] & ({} | null) +>undefined : undefined + + store.setState(key, value); +>store.setState(key, value) : void +>store.setState : (key: K_1, value: S[K_1]) => void +>store : Store +>setState : (key: K_1, value: S[K_1]) => void +>key : K +>value : P[K] & ({} | null) +} + diff --git a/tests/cases/compiler/indexedAccessAndNullableNarrowing.ts b/tests/cases/compiler/indexedAccessAndNullableNarrowing.ts new file mode 100644 index 00000000000..2b402edbc1e --- /dev/null +++ b/tests/cases/compiler/indexedAccessAndNullableNarrowing.ts @@ -0,0 +1,47 @@ +// @strict: true +// @noEmit: true + +function f1, K extends keyof T>(x: T[K] | undefined) { + if (x === undefined) return; + x; // T[K] & ({} | null) + if (x === undefined) return; + x; // T[K] & ({} | null) +} + +function f2, K extends keyof T>(x: T[K] | null) { + if (x === null) return; + x; // T[K] & ({} | undefined) + if (x === null) return; + x; // T[K] & ({} | undefined) +} + +function f3(t: T[K], p1: Partial[K] & {}, p2: Partial[K] & ({} | null)) { + t = p1; + t = p2; +} + +// https://github.com/microsoft/TypeScript/issues/57693 + +type AnyObject = Record; +type State = AnyObject; + +declare function hasOwnProperty( + object: T, + prop: PropertyKey, +): prop is keyof T; + +interface Store { + setState(key: K, value: S[K]): void; +} + +export function syncStoreProp< + S extends State, + P extends Partial, + K extends keyof S, +>(store: Store, props: P, key: K) { + const value = hasOwnProperty(props, key) ? props[key] : undefined; + if (value === undefined) return; + store.setState(key, value); + if (value === undefined) return; + store.setState(key, value); +}