Skip to content

Improve generic keyof relationship checking for generic conditionals with keyless branch types #49563

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<T>`,
// 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.
Expand Down
27 changes: 27 additions & 0 deletions tests/baselines/reference/keyofNonNullableAssignments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//// [keyofNonNullableAssignments.ts]
type MyNonNullable<T> = T extends null ? never : T extends undefined ? never : T;

function f<T>(x: T) {
const a: keyof T = (null as any as keyof NonNullable<T>);
const b: keyof T = (null as any as keyof NonNullable<T & object>);
const c: keyof T = (null as any as keyof MyNonNullable<T>);
const d: keyof T = (null as any as keyof MyNonNullable<T & object>);
const e: keyof T = (null as any as keyof NonNullable<T | undefined>);
const f: keyof T = (null as any as keyof NonNullable<(T | undefined) & object>);
const g: keyof T = (null as any as keyof MyNonNullable<T | undefined>);
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;
}
63 changes: 63 additions & 0 deletions tests/baselines/reference/keyofNonNullableAssignments.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
=== tests/cases/compiler/keyofNonNullableAssignments.ts ===
type MyNonNullable<T> = 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<T>(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<T>);
>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<T & object>);
>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<T>);
>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<T & object>);
>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<T | undefined>);
>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<T | undefined>);
>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))
}

66 changes: 66 additions & 0 deletions tests/baselines/reference/keyofNonNullableAssignments.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
=== tests/cases/compiler/keyofNonNullableAssignments.ts ===
type MyNonNullable<T> = T extends null ? never : T extends undefined ? never : T;
>MyNonNullable : MyNonNullable<T>
>null : null

function f<T>(x: T) {
>f : <T>(x: T) => void
>x : T

const a: keyof T = (null as any as keyof NonNullable<T>);
>a : keyof T
>(null as any as keyof NonNullable<T>) : keyof T
>null as any as keyof NonNullable<T> : keyof T
>null as any : any
>null : null

const b: keyof T = (null as any as keyof NonNullable<T & object>);
>b : keyof T
>(null as any as keyof NonNullable<T & object>) : keyof T
>null as any as keyof NonNullable<T & object> : keyof T
>null as any : any
>null : null

const c: keyof T = (null as any as keyof MyNonNullable<T>);
>c : keyof T
>(null as any as keyof MyNonNullable<T>) : keyof MyNonNullable<T>
>null as any as keyof MyNonNullable<T> : keyof MyNonNullable<T>
>null as any : any
>null : null

const d: keyof T = (null as any as keyof MyNonNullable<T & object>);
>d : keyof T
>(null as any as keyof MyNonNullable<T & object>) : keyof MyNonNullable<T & object>
>null as any as keyof MyNonNullable<T & object> : keyof MyNonNullable<T & object>
>null as any : any
>null : null

const e: keyof T = (null as any as keyof NonNullable<T | undefined>);
>e : keyof T
>(null as any as keyof NonNullable<T | undefined>) : keyof T
>null as any as keyof NonNullable<T | undefined> : 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<T | undefined>);
>g : keyof T
>(null as any as keyof MyNonNullable<T | undefined>) : keyof MyNonNullable<T>
>null as any as keyof MyNonNullable<T | undefined> : keyof MyNonNullable<T>
>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<T & object>
>null as any as keyof MyNonNullable<(T | undefined) & object> : keyof MyNonNullable<T & object>
>null as any : any
>null : null
}

13 changes: 13 additions & 0 deletions tests/cases/compiler/keyofNonNullableAssignments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// @strict: true
type MyNonNullable<T> = T extends null ? never : T extends undefined ? never : T;

function f<T>(x: T) {
const a: keyof T = (null as any as keyof NonNullable<T>);
const b: keyof T = (null as any as keyof NonNullable<T & object>);
const c: keyof T = (null as any as keyof MyNonNullable<T>);
const d: keyof T = (null as any as keyof MyNonNullable<T & object>);
const e: keyof T = (null as any as keyof NonNullable<T | undefined>);
const f: keyof T = (null as any as keyof NonNullable<(T | undefined) & object>);
const g: keyof T = (null as any as keyof MyNonNullable<T | undefined>);
const h: keyof T = (null as any as keyof MyNonNullable<(T | undefined) & object>);
}