-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
|
@@ -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)) { | ||
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. | ||
|
@@ -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) : | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small change to falsy flag calculation to pick up |
||
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 : | ||
|
@@ -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); | ||
} | ||
|
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; | ||
} |
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)) | ||
} |
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 | ||
} |
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { } |
There was a problem hiding this comment.
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 thetypeof 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.