diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 9e56c58411930..10c600a07d239 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -19269,6 +19269,21 @@ namespace ts { return result; } + function removeIntersectionMembersWithoutKeys(type: Type) { + if (!(type.flags & TypeFlags.Intersection)) { + return type; + } + return getIntersectionType(filter((type as IntersectionType).types, t => !isKeylessType(t))); + } + + function isKeylessType(type: Type) { + return !!(getIndexType(type).flags & TypeFlags.Never); + } + + function isConditionalFilteringKeylessTypes(type: Type) { + return !!(type.flags & TypeFlags.Conditional) && (type as ConditionalType).root.isDistributive && everyType((type as ConditionalType).extendsType, isKeylessType); + } + function structuredTypeRelatedTo(source: Type, target: Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary { if (intersectionState & IntersectionState.PropertyCheck) { return propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, IntersectionState.None); @@ -19411,9 +19426,34 @@ namespace ts { const targetType = (target as IndexType).type; // A keyof S is related to a keyof T if T is related to S. if (sourceFlags & TypeFlags.Index) { - if (result = isRelatedTo(targetType, (source as IndexType).type, RecursionFlags.Both, /*reportErrors*/ false)) { + let sourceType = (source as IndexType).type; + if (result = isRelatedTo(targetType, sourceType, RecursionFlags.Both, /*reportErrors*/ false)) { return result; } + // If the source is a filtering conditional that removes only keyless sources, eg, `NonNullable`, + // then it doesn't affect the `keyof` query, and we can unwrap the conditional and relate the unwrapped source and target. + // There may be multiple stacked conditionals, such as `T extends null ? never : T extends undefined ? never : T : T`, so + // we need to repeat the unwrapping process. + while (isConditionalFilteringKeylessTypes(sourceType)) { + const lastSource = sourceType; + sourceType = getDefaultConstraintOfConditionalType(sourceType as ConditionalType); + if (sourceType === lastSource) { + break; + } + if (result = isRelatedTo(targetType, sourceType, RecursionFlags.Both, /*reportErrors*/ false)) { + return result; + } + const simplifiedSource = sourceType; + // In addition, `keyof (T & U)` is equivalent to `keyof T | keyof U`, so if `keyof U` is always `never`, we can omit + // it from the relationship. This allows, eg, `keyof (T & object)` to be related to `keyof T`. + sourceType = removeIntersectionMembersWithoutKeys(sourceType); + if (sourceType === simplifiedSource) { + continue; + } + if (result = isRelatedTo(targetType, sourceType, RecursionFlags.Both, /*reportErrors*/ false)) { + return result; + } + } } if (isTupleType(targetType)) { // An index type can have a tuple type target when the tuple type contains variadic elements. diff --git a/tests/baselines/reference/keyofNonNullableAssignments.js b/tests/baselines/reference/keyofNonNullableAssignments.js new file mode 100644 index 0000000000000..6d570b8a7b159 --- /dev/null +++ b/tests/baselines/reference/keyofNonNullableAssignments.js @@ -0,0 +1,27 @@ +//// [keyofNonNullableAssignments.ts] +type MyNonNullable = T extends null ? never : T extends undefined ? never : T; + +function f(x: T) { + const a: keyof T = (null as any as keyof NonNullable); + const b: keyof T = (null as any as keyof NonNullable); + const c: keyof T = (null as any as keyof MyNonNullable); + const d: keyof T = (null as any as keyof MyNonNullable); + const e: keyof T = (null as any as keyof NonNullable); + const f: keyof T = (null as any as keyof NonNullable<(T | undefined) & object>); + const g: keyof T = (null as any as keyof MyNonNullable); + const h: keyof T = (null as any as keyof MyNonNullable<(T | undefined) & object>); +} + + +//// [keyofNonNullableAssignments.js] +"use strict"; +function f(x) { + var a = null; + var b = null; + var c = null; + var d = null; + var e = null; + var f = null; + var g = null; + var h = null; +} diff --git a/tests/baselines/reference/keyofNonNullableAssignments.symbols b/tests/baselines/reference/keyofNonNullableAssignments.symbols new file mode 100644 index 0000000000000..dcdb2afc4eb4f --- /dev/null +++ b/tests/baselines/reference/keyofNonNullableAssignments.symbols @@ -0,0 +1,63 @@ +=== tests/cases/compiler/keyofNonNullableAssignments.ts === +type MyNonNullable = T extends null ? never : T extends undefined ? never : T; +>MyNonNullable : Symbol(MyNonNullable, Decl(keyofNonNullableAssignments.ts, 0, 0)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 0, 19)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 0, 19)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 0, 19)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 0, 19)) + +function f(x: T) { +>f : Symbol(f, Decl(keyofNonNullableAssignments.ts, 0, 81)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 2, 11)) +>x : Symbol(x, Decl(keyofNonNullableAssignments.ts, 2, 14)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 2, 11)) + + const a: keyof T = (null as any as keyof NonNullable); +>a : Symbol(a, Decl(keyofNonNullableAssignments.ts, 3, 9)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 2, 11)) +>NonNullable : Symbol(NonNullable, Decl(lib.es5.d.ts, --, --)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 2, 11)) + + const b: keyof T = (null as any as keyof NonNullable); +>b : Symbol(b, Decl(keyofNonNullableAssignments.ts, 4, 9)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 2, 11)) +>NonNullable : Symbol(NonNullable, Decl(lib.es5.d.ts, --, --)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 2, 11)) + + const c: keyof T = (null as any as keyof MyNonNullable); +>c : Symbol(c, Decl(keyofNonNullableAssignments.ts, 5, 9)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 2, 11)) +>MyNonNullable : Symbol(MyNonNullable, Decl(keyofNonNullableAssignments.ts, 0, 0)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 2, 11)) + + const d: keyof T = (null as any as keyof MyNonNullable); +>d : Symbol(d, Decl(keyofNonNullableAssignments.ts, 6, 9)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 2, 11)) +>MyNonNullable : Symbol(MyNonNullable, Decl(keyofNonNullableAssignments.ts, 0, 0)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 2, 11)) + + const e: keyof T = (null as any as keyof NonNullable); +>e : Symbol(e, Decl(keyofNonNullableAssignments.ts, 7, 9)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 2, 11)) +>NonNullable : Symbol(NonNullable, Decl(lib.es5.d.ts, --, --)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 2, 11)) + + const f: keyof T = (null as any as keyof NonNullable<(T | undefined) & object>); +>f : Symbol(f, Decl(keyofNonNullableAssignments.ts, 8, 9)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 2, 11)) +>NonNullable : Symbol(NonNullable, Decl(lib.es5.d.ts, --, --)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 2, 11)) + + const g: keyof T = (null as any as keyof MyNonNullable); +>g : Symbol(g, Decl(keyofNonNullableAssignments.ts, 9, 9)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 2, 11)) +>MyNonNullable : Symbol(MyNonNullable, Decl(keyofNonNullableAssignments.ts, 0, 0)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 2, 11)) + + const h: keyof T = (null as any as keyof MyNonNullable<(T | undefined) & object>); +>h : Symbol(h, Decl(keyofNonNullableAssignments.ts, 10, 9)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 2, 11)) +>MyNonNullable : Symbol(MyNonNullable, Decl(keyofNonNullableAssignments.ts, 0, 0)) +>T : Symbol(T, Decl(keyofNonNullableAssignments.ts, 2, 11)) +} + diff --git a/tests/baselines/reference/keyofNonNullableAssignments.types b/tests/baselines/reference/keyofNonNullableAssignments.types new file mode 100644 index 0000000000000..4cb3d96a8f3b7 --- /dev/null +++ b/tests/baselines/reference/keyofNonNullableAssignments.types @@ -0,0 +1,66 @@ +=== tests/cases/compiler/keyofNonNullableAssignments.ts === +type MyNonNullable = T extends null ? never : T extends undefined ? never : T; +>MyNonNullable : MyNonNullable +>null : null + +function f(x: T) { +>f : (x: T) => void +>x : T + + const a: keyof T = (null as any as keyof NonNullable); +>a : keyof T +>(null as any as keyof NonNullable) : keyof T +>null as any as keyof NonNullable : keyof T +>null as any : any +>null : null + + const b: keyof T = (null as any as keyof NonNullable); +>b : keyof T +>(null as any as keyof NonNullable) : keyof T +>null as any as keyof NonNullable : keyof T +>null as any : any +>null : null + + const c: keyof T = (null as any as keyof MyNonNullable); +>c : keyof T +>(null as any as keyof MyNonNullable) : keyof MyNonNullable +>null as any as keyof MyNonNullable : keyof MyNonNullable +>null as any : any +>null : null + + const d: keyof T = (null as any as keyof MyNonNullable); +>d : keyof T +>(null as any as keyof MyNonNullable) : keyof MyNonNullable +>null as any as keyof MyNonNullable : keyof MyNonNullable +>null as any : any +>null : null + + const e: keyof T = (null as any as keyof NonNullable); +>e : keyof T +>(null as any as keyof NonNullable) : keyof T +>null as any as keyof NonNullable : keyof T +>null as any : any +>null : null + + const f: keyof T = (null as any as keyof NonNullable<(T | undefined) & object>); +>f : keyof T +>(null as any as keyof NonNullable<(T | undefined) & object>) : keyof T +>null as any as keyof NonNullable<(T | undefined) & object> : keyof T +>null as any : any +>null : null + + const g: keyof T = (null as any as keyof MyNonNullable); +>g : keyof T +>(null as any as keyof MyNonNullable) : keyof MyNonNullable +>null as any as keyof MyNonNullable : keyof MyNonNullable +>null as any : any +>null : null + + const h: keyof T = (null as any as keyof MyNonNullable<(T | undefined) & object>); +>h : keyof T +>(null as any as keyof MyNonNullable<(T | undefined) & object>) : keyof MyNonNullable +>null as any as keyof MyNonNullable<(T | undefined) & object> : keyof MyNonNullable +>null as any : any +>null : null +} + diff --git a/tests/cases/compiler/keyofNonNullableAssignments.ts b/tests/cases/compiler/keyofNonNullableAssignments.ts new file mode 100644 index 0000000000000..a35241588a5b8 --- /dev/null +++ b/tests/cases/compiler/keyofNonNullableAssignments.ts @@ -0,0 +1,13 @@ +// @strict: true +type MyNonNullable = T extends null ? never : T extends undefined ? never : T; + +function f(x: T) { + const a: keyof T = (null as any as keyof NonNullable); + const b: keyof T = (null as any as keyof NonNullable); + const c: keyof T = (null as any as keyof MyNonNullable); + const d: keyof T = (null as any as keyof MyNonNullable); + const e: keyof T = (null as any as keyof NonNullable); + const f: keyof T = (null as any as keyof NonNullable<(T | undefined) & object>); + const g: keyof T = (null as any as keyof MyNonNullable); + const h: keyof T = (null as any as keyof MyNonNullable<(T | undefined) & object>); +}