Closed jedwards1211 closed 3 weeks ago
Which one are you asking us to change?
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.
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 }`
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.
I recently introduced changes to https://github.com/microsoft/TypeScript/pull/52095 that fix this issue
@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}
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.
@ahejlsberg could you take a look at my PR? it already has this logic: https://github.com/microsoft/TypeScript/pull/52095/files#diff-d9ab6589e714c71e657f601cf30ff51dfc607fc98419bf72e04f6b0fa92cc4b8R31639-R31643
@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.
Makes sense, thank y'all!
🔎 Search Terms
object intersection literal inference any
🕗 Version & Regression Information
⏯ 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:
Original example:
🙁 Actual behavior
🙂 Expected behavior
No error and no difference in the reported type for the
ml
property in JSX andMyProps['ml']
Additional information about the issue
I hope that literal inference can be improved to handle this case without an error:
Two potential solutions I proposed below are: