Skip to content
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

Incorrect object properties type inference with aliased conditional expressions #45624

Closed
ruizb opened this issue Aug 29, 2021 · 3 comments
Closed
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@ruizb
Copy link

ruizb commented Aug 29, 2021

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.

@andrewbranch andrewbranch added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Sep 7, 2021
@andrewbranch
Copy link
Member

Not related to aliased conditions; inlining the conditions into the if has the same result. When we narrow x.name, we don’t actually incorporate that information back into the type of x; we only see that narrowing when we get the type of a fully-matching reference to x.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
Copy link
Author

ruizb commented 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
Copy link
Member

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.

@ruizb ruizb closed this as completed Nov 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

2 participants