Repository navigation
Incorrect object properties type inference with aliased conditional expressions #45624
Description
Bug Report
🔎 Search Terms
type inference, inference, aliased condition, aliased conditional expression
🕗 Version & Regression Information
It started happening when the aliased conditional expressions were introduced in the following pull-request:
This issue is still present as of v4.4.2.
I was unable to test this on prior versions because aliased conditional expressions didn't exist previously.
⏯ Playground Link
Playground link with relevant code.
💻 Code
function hasOwnProperty<X extends {}, Y extends PropertyKey>
(obj: X, prop: Y): obj is X & Readonly<Record<Y, unknown>> {
return obj.hasOwnProperty(prop)
}
const isObject = (val: unknown): val is object => val !== null && typeof val === 'object'
const isString = (val: unknown): val is string => typeof val === 'string'
declare const x: unknown
const isAnObject = isObject(x)
const hasValidName = isAnObject && hasOwnProperty(x, 'name') && isString(x.name)
if (hasValidName) {
const res0 = x // object & Readonly<Record<"name", unknown>>
const res1 = x.name // string
}If it can help, I generated a .types file as well.
See the .types file contents.
=== tests/cases/compiler/_baguette.ts ===
function hasOwnProperty<X extends {}, Y extends PropertyKey>
>hasOwnProperty : <X extends {}, Y extends PropertyKey>(obj: X, prop: Y) => obj is X & Readonly<Record<Y, unknown>>
(obj: X, prop: Y): obj is X & Readonly<Record<Y, unknown>> {
>obj : X
>prop : Y
return obj.hasOwnProperty(prop)
>obj.hasOwnProperty(prop) : boolean
>obj.hasOwnProperty : (v: PropertyKey) => boolean
>obj : X
>hasOwnProperty : (v: PropertyKey) => boolean
>prop : PropertyKey
}
const isObject = (val: unknown): val is object => val !== null && typeof val === 'object'
>isObject : (val: unknown) => val is object
>(val: unknown): val is object => val !== null && typeof val === 'object' : (val: unknown) => val is object
>val : unknown
>val !== null && typeof val === 'object' : boolean
>val !== null : boolean
>val : unknown
>null : null
>typeof val === 'object' : boolean
>typeof val : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>val : unknown
>'object' : "object"
const isString = (val: unknown): val is string => typeof val === 'string'
>isString : (val: unknown) => val is string
>(val: unknown): val is string => typeof val === 'string' : (val: unknown) => val is string
>val : unknown
>typeof val === 'string' : boolean
>typeof val : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>val : unknown
>'string' : "string"
declare const x: unknown
>x : unknown
const isAnObject = isObject(x)
>isAnObject : boolean
>isObject(x) : boolean
>isObject : (val: unknown) => val is object
>x : unknown
const hasValidName = isAnObject && hasOwnProperty(x, 'name') && isString(x.name)
>hasValidName : boolean
>isAnObject && hasOwnProperty(x, 'name') && isString(x.name) : boolean
>isAnObject && hasOwnProperty(x, 'name') : boolean
>isAnObject : boolean
>hasOwnProperty(x, 'name') : boolean
>hasOwnProperty : <X extends {}, Y extends PropertyKey>(obj: X, prop: Y) => obj is X & Readonly<Record<Y, unknown>>
>x : object
>'name' : "name"
>isString(x.name) : boolean
>isString : (val: unknown) => val is string
>x.name : unknown
>x : object & Readonly<Record<"name", unknown>>
>name : unknown
if (hasValidName) {
>hasValidName : boolean
const res0 = x
>res0 : object & Readonly<Record<"name", unknown>>
>x : object & Readonly<Record<"name", unknown>>
const res1 = x.name
>res1 : string
>x.name : string
>x : object & Readonly<Record<"name", unknown>>
>name : string
}
🙁 Actual behavior
The type of the name property of x is inferred as unknown (cf. res0).
However, when accessing the property with x.name (cf. res1), its type is correctly inferred as string.
🙂 Expected behavior
The name property should be inferred as string when checking the properties of x.
For example, as a developer, when typing x., I'm expecting to see name: string in the "auto-completion" list, instead of name: unknown.
Activity
andrewbranch commentedon Sep 7, 2021
Not related to aliased conditions; inlining the conditions into the
ifhas the same result. When we narrowx.name, we don’t actually incorporate that information back into the type ofx; we only see that narrowing when we get the type of a fully-matching reference tox.name. I agree the completion list behavior is confusing—it looks like we’re showing the declared type, not the control flow type, of each property. (Or more likely, we are asking for the control flow type, but because there is no actual referencing node yet, the narrowing logic doesn’t see that there’s a hypothetical matching reference to the property that can be narrowed. This is often a problem with completions, where the checker is ill-adapted to thinking about the type state that will happen if some code were inserted.) But if that’s the case, it would likely be prohibitively expensive to try to do narrowing on every property in completion lists. 😕ruizb commentedon Sep 25, 2021
Thanks for the explanation Andrew!
Why would it be "prohibitively expensive" to try narrowing the type in such situation? Is it because a property could have a "deep" narrowed type, which would take a significant amount of time to the type checker?
Do you think we could somehow force the type narrowing anyway? Via some editor plugin, or a mystical TS configuration maybe 😛
(also I guess we can close this issue as there isn't any actual bug)
andrewbranch commentedon Sep 30, 2021
Completions lists need to be really fast since they’re an interactive editor feature that pop up as you type, and control flow narrowing can be expensive. You’d have to repeat the process for every single property in the list, so it’s an extremely hot path. Even if it’s only a little bit more work per item, you’re often multiplying that work by 10x–100x (the size of the list) as the user types.