Skip to content

In typeof x === object, type parameters extending unknown narrow like unknown itself #49091

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

Closed
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
55 changes: 53 additions & 2 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19224,6 +19224,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 @@ -19366,9 +19381,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)) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These keyof relationship improvements aren't strictly speaking tied to the typeof x === "object" narrowing change, but they do improve common uses of it (eg, narrowing within a loop and doing an index operation), so I think make sense to pair with it.

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 Expand Up @@ -21330,11 +21370,20 @@ namespace ts {
return result;
}

function getApparentIntersectionTypes(type: IntersectionType): Type[] {
const apparent = getApparentType(type);
if (apparent.flags & TypeFlags.Intersection) {
return (apparent as IntersectionType).types;
}
return [apparent];
}

// Returns the String, Number, Boolean, StringLiteral, NumberLiteral, BooleanLiteral, Void, Undefined, or Null
// flags for the string, number, boolean, "", 0, false, void, undefined, or null types respectively. Returns
// no flags for all other types (including non-falsy literal types).
function getFalsyFlags(type: Type): TypeFlags {
return type.flags & TypeFlags.Union ? getFalsyFlagsOfTypes((type as UnionType).types) :
type.flags & TypeFlags.Intersection ? reduceLeft(getApparentIntersectionTypes(type as IntersectionType), (memo, type) => memo | getFalsyFlags(type), 0 as TypeFacts) :
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small change to falsy flag calculation to pick up T & undefined as falsy when it doesn't reduce to never.

type.flags & TypeFlags.StringLiteral ? (type as StringLiteralType).value === "" ? TypeFlags.StringLiteral : 0 :
type.flags & TypeFlags.NumberLiteral ? (type as NumberLiteralType).value === 0 ? TypeFlags.NumberLiteral : 0 :
type.flags & TypeFlags.BigIntLiteral ? isZeroBigInt(type as BigIntLiteralType) ? TypeFlags.BigIntLiteral : 0 :
Expand Down Expand Up @@ -25019,7 +25068,9 @@ namespace ts {
case "function":
return type.flags & TypeFlags.Any ? type : globalFunctionType;
case "object":
return type.flags & TypeFlags.Unknown ? getUnionType([nonPrimitiveType, nullType]) : type;
const isUnknownish = type.flags & TypeFlags.Unknown ||
(type.flags & TypeFlags.InstantiableNonPrimitive && (getBaseConstraintOfType(type) || unknownType).flags & TypeFlags.Unknown);
return isUnknownish ? getUnionType([nonPrimitiveType, nullType]) : type;
default:
return typeofTypesByName.get(text);
}
Expand Down
26 changes: 26 additions & 0 deletions tests/baselines/reference/keyofNonNullableAssignments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//// [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;
}
62 changes: 62 additions & 0 deletions tests/baselines/reference/keyofNonNullableAssignments.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
=== 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))
}
65 changes: 65 additions & 0 deletions tests/baselines/reference/keyofNonNullableAssignments.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
=== 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 NonNullable<T>
>null as any as keyof NonNullable<T> : keyof NonNullable<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 NonNullable<T & object>
>null as any as keyof NonNullable<T & object> : keyof NonNullable<T & object>
>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 NonNullable<T>
>null as any as keyof NonNullable<T | undefined> : keyof NonNullable<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 NonNullable<T & object>
>null as any as keyof NonNullable<(T | undefined) & object> : keyof NonNullable<T & object>
>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
}
20 changes: 10 additions & 10 deletions tests/baselines/reference/mappedTypes4.types
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,19 @@ function boxify<T>(obj: T): Boxified<T> {
>{} : {}

for (let k in obj) {
>k : Extract<keyof T, string>
>obj : T
>k : Extract<keyof NonNullable<T & object>, string>
>obj : T & (object | null)

result[k] = { value: obj[k] };
>result[k] = { value: obj[k] } : { value: T[Extract<keyof T, string>]; }
>result[k] : Boxified<T>[Extract<keyof T, string>]
>result[k] = { value: obj[k] } : { value: NonNullable<T & object>[Extract<keyof NonNullable<T & object>, string>]; }
>result[k] : Boxified<T>[Extract<keyof NonNullable<T & object>, string>]
>result : Boxified<T>
>k : Extract<keyof T, string>
>{ value: obj[k] } : { value: T[Extract<keyof T, string>]; }
>value : T[Extract<keyof T, string>]
>obj[k] : T[Extract<keyof T, string>]
>obj : T
>k : Extract<keyof T, string>
>k : Extract<keyof NonNullable<T & object>, string>
>{ value: obj[k] } : { value: NonNullable<T & object>[Extract<keyof NonNullable<T & object>, string>]; }
>value : NonNullable<T & object>[Extract<keyof NonNullable<T & object>, string>]
>obj[k] : NonNullable<T & object>[Extract<keyof NonNullable<T & object>, string>]
>obj : NonNullable<T & object>
>k : Extract<keyof NonNullable<T & object>, string>
}
return result;
>result : Boxified<T>
Expand Down
55 changes: 55 additions & 0 deletions tests/baselines/reference/unconstrainedTypeParameterNarrowing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//// [unconstrainedTypeParameterNarrowing.ts]
function f1<T>(x: T) {
if (typeof x === "object" && x) {
g(x);
}
}

function f2<T extends unknown>(x: T) {
if (typeof x === "object" && x) {
g(x);
}
}

// #48468 but with an explicit constraint so as to not trigger the `{}` and unconstrained type parameter bug
function deepEquals<T extends unknown>(a: T, b: T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you have a test for both the explicit and non-explicit constraint?

FWIW I have tests at #48576.

if (typeof a !== "object" || typeof b !== "object" || !a || !b) {
return false;
}
if (Array.isArray(a) || Array.isArray(b)) {
return false;
}
if (Object.keys(a).length !== Object.keys(b).length) {
return false;
}
return true;
}

function g(x: object) {}

//// [unconstrainedTypeParameterNarrowing.js]
"use strict";
function f1(x) {
if (typeof x === "object" && x) {
g(x);
}
}
function f2(x) {
if (typeof x === "object" && x) {
g(x);
}
}
// #48468 but with an explicit constraint so as to not trigger the `{}` and unconstrained type parameter bug
function deepEquals(a, b) {
if (typeof a !== "object" || typeof b !== "object" || !a || !b) {
return false;
}
if (Array.isArray(a) || Array.isArray(b)) {
return false;
}
if (Object.keys(a).length !== Object.keys(b).length) {
return false;
}
return true;
}
function g(x) { }
Loading