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

Literal inference causes unexpected errors on a values that trivially satisfy compound type intersections containing any #59473

Closed
jedwards1211 opened this issue Jul 30, 2024 · 10 comments Β· Fixed by #52095
Assignees
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue

Comments

@jedwards1211
Copy link

jedwards1211 commented Jul 30, 2024

πŸ”Ž Search Terms

object intersection literal inference any

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about intersections and inference

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.5.4#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wChSB6CuGAC2AGc41ckntc4UA7OCAGwAmcAG5IoDYBF4RMcALIBVAJKkYATzBI4AIQgAPAAo4wTALxwA3qThwQ-APwAuLt3VwqcZXEHBB3fHgiFH5gAC9tOkYaTW0AdxQmHnVSAF9yTwAVeiZBCDYA+AYAVzQ0NgZMYv5+dwgxKCg-SNptMBNxDRitV2FvNB44TGB9OGAYADpKah1i+DptcRwoOwqUAHMW6IHihiRhBKYWbirJbnXeuG4kfaZQdvqkECRuGDVYhXVjCFM4Cz0jCYmAAyKw2OyOFz4fabfBwAA+V2KIAARuI0uRjgx4PJ1ABhXBgP5wAAUD1MLlx31MAEo-gA+JE1cgaHqZYlUoEAbXw9nwAF0PNRkpjpNi4KMLAAeXEE8AQswAIhhSEVHkZWViTFkcHJnXcvP4cJQRDG3BY4BQMGAKP4SCmtk1PXw2Ka5zh0W4EHgiUk624KFtkQg3W0+G4yLRK0RysEmzViOK3EESGG10E+AmJIATABmbPZmkZagAdXoaFoYzuMActmUTAABvYXMkG3wVtFxnAGxHUeIEXBY-GB0mU2n9g2HEA

πŸ’» Code

EDIT: simplified examples:

const a: [any] & [1] = [1] // Type '[number]' is not assignable to type '[1]'
const b: { ml: any } & { ml: 'edge' } = { ml: 'edge' } // Type '{ ml: string; }' is not assignable to type '{ ml: any; } & { ml: "edge"; }'

Original example:

import React from 'react'

// this comes from an old version of MUI
type BoxProps = {
  ml?: any // I didn't realize this type was any
}

// This doesn't successfully override the property type and I can fix it.
// But the error message this caused was nonsensical and needs improvement
type MyProps = BoxProps & {
  ml?: 'edge' | number
}

const MyComp = (props: MyProps) => null

type T = MyProps['ml'] // any

const x = <MyComp ml="edge" /> // Types of property 'ml' are incompatible.
  // Type 'string' is not assignable to type 'number | "edge" | undefined'.(2322)

// Which is it?  Is `ml: any` or is it `number | "edge" | undefined`?

πŸ™ Actual behavior

Type '{ ml: string; }' is not assignable to type '{ ml?: number | "edge" | undefined; }'.
  Types of property 'ml' are incompatible.
    Type 'string' is not assignable to type 'number | "edge" | undefined'.(2322)

πŸ™‚ Expected behavior

No error and no difference in the reported type for the ml property in JSX and MyProps['ml']

Additional information about the issue

I hope that literal inference can be improved to handle this case without an error:

const x: { ml: any } & { ml: 'edge' } = { ml: 'edge' }

Two potential solutions I proposed below are:

  1. Run a separate literal inference on each half of the required intersection type, then intersect the inferred types
  2. Fall back to unwidened type if it satisfies the required type but literal inference doesn't
@jedwards1211 jedwards1211 changed the title Weird error for JSX property type that was accidentally intersected with any Weird error for JSX property type intersected with any Jul 30, 2024
@RyanCavanaugh
Copy link
Member

Which one are you asking us to change?

@jedwards1211
Copy link
Author

jedwards1211 commented Aug 2, 2024

It seems like the crux of the problem here is the way literal inference works, I wish it would infer a type of { ml: 'edge' } in this case. The compiler seems to be inferring the type of value { ml: 'edge' } against the resolved type of the intersection ({ ml: any }), resulting in the type { ml: string } instead, maybe analogous to this:

type Constraint = { ml: any } & { ml: 'edge' }
type Resolved = { [K in keyof Constraint]: Constraint[K] } // { ml: any }
const Inferred = { ml: 'edge' } satisfies Resolved // { ml: string }
const Checked: Constraint = Inferred // Type '{ ml: string; }' is not assignable to type '{ ml: "edge"; }'

What if instead, literal inference was done against both sides of the intersection, and then the results were intersected?

type Constraint = { ml: any } & { ml: 'edge' }
const InferredLeft = { ml: 'edge' } satisfies { ml: any } // { ml: string }
const InferredRight = { ml: 'edge' } satisfies { ml: 'edge' } // { ml: 'edge' }
const Inferred: typeof InferredLeft & typeof InferredRight = { ml: 'edge' }
const Checked: Constraint = Inferred // no error!

Maybe this approach would cause other problems though, I haven't thought of one yet.

Or maybe another option would be, if the initial literal inference type doesn't satisfy the constraint, the compiler tries the unwidened type (just { ml: 'edge' }) as a fallback, and uses that instead if it satisfies the constraint?

In most cases literal inference behaves in harmony with how type checking works, but in this case it feels pretty dissonant:

const x: { ml: any } & { ml: 'edge' } = { ml: 'edge' } // Type '{ ml: string; }' is not assignable to type '{ ml: any; } & { ml: "edge"; }'

It seems not ideal for a RHS that satisfies the LHS type as trivially as this to be an error.

@jedwards1211 jedwards1211 changed the title Weird error for JSX property type intersected with any Literal inference causes unexpected error on a value that trivially satisfies object intersection type Aug 2, 2024
@jedwards1211
Copy link
Author

jedwards1211 commented Aug 2, 2024

Also I wish that literal inference wouldn't behave so differently for primitive and compound types:

const a = 1 satisfies any & 1 // type of a: 1
const b = [1] satisfies [any & 1]  // type of b: [number]
const c = { c: 1 } satisfies { c: any & 1 } // type of c: { c: number }

// would have expected either `number/[number]/{ c: number }` or `1/[1]/{ c: 1 }`

@jedwards1211 jedwards1211 changed the title Literal inference causes unexpected error on a value that trivially satisfies object intersection type Literal inference causes unexpected errors on a values that trivially satisfy compound type intersections with any Aug 2, 2024
@jedwards1211 jedwards1211 changed the title Literal inference causes unexpected errors on a values that trivially satisfy compound type intersections with any Literal inference causes unexpected errors on a values that trivially satisfy compound type intersections containing any Aug 2, 2024
@jedwards1211
Copy link
Author

jedwards1211 commented Aug 2, 2024

Btw I don't mean to be pedantic here; the practical downside I experienced from the current behavior is, another dev on my team was so confused by the error message they gave up and used // @ts-expect-error.
I realized that I had better fix our type declaration to work correctly, but it took me awhile to understand what the problem was too.

@Andarist
Copy link
Contributor

Andarist commented Aug 3, 2024

I recently introduced changes to #52095 that fix this issue

@jedwards1211
Copy link
Author

jedwards1211 commented Aug 3, 2024

@Andarist from reading the description that sounds different to me, since there are no indexed types or mapped types in my minimal repro here, but maybe your PR has additional effects?

And am I correct in my assumption that this issue stems from current literal inference behavior?

I think all of the following would have to work for this issue to be considered fixed (I would reopen if not):

const a: [any] & [1] = [1]
const b: any[] & 1[] = [1, 1]
const c: {a: any} & {a: 1} = {a: 1}

@ahejlsberg
Copy link
Member

The issue here is our treatment of any in intersections: We say that an intersection that includes any resolves to any. For example, any & 1 resolves to any. That's consistent with our treatment of never (never & 1 resolves to never), but not with our treatment of unknown (unknown & 1 resolves to 1). Since any is a bit of both it's debatable what's "right", but realistically we can't change it since way too much code depends on the existing behavior.

That said, we can change the behavior of any in intersections of contextual element types. For example, an element of an array contextually typed by [any] & [1] really should have the contextual type 1 such that literal types are preserved. In fact, we really want any to behave as unknown in this scenario such that we preserve the most specific type possible. It's similar to how we don't do subtype reduction for contextual union types in the interest of preserving the most specific types.

I'll put up a PR that implements this.

@Andarist
Copy link
Contributor

Andarist commented Aug 3, 2024

@ahejlsberg
Copy link
Member

@Andarist Yes, I noticed that PR but it seemed more complex that I think it necessary. I have just put up #59528, let me know if you think it is missing anything essential.

@ahejlsberg ahejlsberg added Bug A bug in TypeScript and removed Needs Investigation This issue needs a team member to investigate its status. labels Aug 3, 2024
@ahejlsberg ahejlsberg added this to the TypeScript 5.7.0 milestone Aug 3, 2024
@jedwards1211
Copy link
Author

Makes sense, thank y'all!

@typescript-bot typescript-bot added the Fix Available A PR has been opened for this issue label Aug 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue
Projects
None yet
5 participants