Skip to content

Commit

Permalink
Fix nullability intersections in CFA and relations (#57724)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahejlsberg authored Mar 11, 2024
1 parent e24d886 commit ef6a4ab
Show file tree
Hide file tree
Showing 4 changed files with 394 additions and 3 deletions.
37 changes: 34 additions & 3 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}
Expand Down
177 changes: 177 additions & 0 deletions tests/baselines/reference/indexedAccessAndNullableNarrowing.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
//// [tests/cases/compiler/indexedAccessAndNullableNarrowing.ts] ////

=== indexedAccessAndNullableNarrowing.ts ===
function f1<T extends Record<string, any>, 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<T extends Record<string, any>, 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, K extends keyof T>(t: T[K], p1: Partial<T>[K] & {}, p2: Partial<T>[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<string, any>;
>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<T extends AnyObject>(
>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<S = State> {
>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<K extends keyof S>(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<S>,
>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<S>, 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))
}

136 changes: 136 additions & 0 deletions tests/baselines/reference/indexedAccessAndNullableNarrowing.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//// [tests/cases/compiler/indexedAccessAndNullableNarrowing.ts] ////

=== indexedAccessAndNullableNarrowing.ts ===
function f1<T extends Record<string, any>, K extends keyof T>(x: T[K] | undefined) {
>f1 : <T extends Record<string, any>, 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<T extends Record<string, any>, K extends keyof T>(x: T[K] | null) {
>f2 : <T extends Record<string, any>, 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, K extends keyof T>(t: T[K], p1: Partial<T>[K] & {}, p2: Partial<T>[K] & ({} | null)) {
>f3 : <T, K extends keyof T>(t: T[K], p1: Partial<T>[K] & {}, p2: Partial<T>[K] & ({} | null)) => void
>t : T[K]
>p1 : Partial<T>[K] & {}
>p2 : Partial<T>[K] & ({} | null)

t = p1;
>t = p1 : Partial<T>[K] & {}
>t : T[K]
>p1 : Partial<T>[K] & {}

t = p2;
>t = p2 : Partial<T>[K] & ({} | null)
>t : T[K]
>p2 : Partial<T>[K] & ({} | null)
}

// https://github.com/microsoft/TypeScript/issues/57693

type AnyObject = Record<string, any>;
>AnyObject : { [x: string]: any; }

type State = AnyObject;
>State : AnyObject

declare function hasOwnProperty<T extends AnyObject>(
>hasOwnProperty : <T extends AnyObject>(object: T, prop: PropertyKey) => prop is keyof T

object: T,
>object : T

prop: PropertyKey,
>prop : PropertyKey

): prop is keyof T;

interface Store<S = State> {
setState<K extends keyof S>(key: K, value: S[K]): void;
>setState : <K extends keyof S>(key: K, value: S[K]) => void
>key : K
>value : S[K]
}

export function syncStoreProp<
>syncStoreProp : <S extends AnyObject, P extends Partial<S>, K extends keyof S>(store: Store<S>, props: P, key: K) => void

S extends State,
P extends Partial<S>,
K extends keyof S,
>(store: Store<S>, props: P, key: K) {
>store : Store<S>
>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 : <T extends AnyObject>(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 : <K_1 extends keyof S>(key: K_1, value: S[K_1]) => void
>store : Store<S>
>setState : <K_1 extends keyof S>(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 : <K_1 extends keyof S>(key: K_1, value: S[K_1]) => void
>store : Store<S>
>setState : <K_1 extends keyof S>(key: K_1, value: S[K_1]) => void
>key : K
>value : P[K] & ({} | null)
}

Loading

0 comments on commit ef6a4ab

Please sign in to comment.