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

Intersection, Union depends on type variable instantiation order #56906

Open
rotu opened this issue Dec 30, 2023 · 7 comments
Open

Intersection, Union depends on type variable instantiation order #56906

rotu opened this issue Dec 30, 2023 · 7 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@rotu
Copy link

rotu commented Dec 30, 2023

🔎 Search Terms

generic type variable intersection union order

🕗 Version & Regression Information

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

⏯ Playground Link

Workbench Repro

💻 Code

type Union<A, B> = A | B
type UnionAny<T> = Union<T, any>
type UnionUnknown<T> = Union<unknown, T>

// these should all be the same type but are respectively: any, any, unknown 
type UA0 = Union<unknown, any>
//   ^?
type UA1 = UnionAny<unknown>
//   ^?
type UA2 = UnionUnknown<any>
//   ^?

type Intersect<A, B> = A & B
type IntersectAny<T> = Intersect<T, any>
type IntersectNever<T> = Intersect<never, T>

// these should all be the same type but are respectively: never, any, never
type AN0 = Intersect<never, any>
//   ^?
type AN1 = IntersectAny<never>
//   ^?
type AN2 = IntersectNever<any>
//   ^?

🙁 Actual behavior

The types UA0, UA1 are resolved as any and UA2 is resolved as unknown. These all map down to the same union of unknown and any, so should all be the same type.

The types AN0, AN2 are resolved as never and AN1 resolved as any. These all map down to be the intersection of any and never, so should all be the same type.

🙂 Expected behavior

I expect UA0, UA1 and UA2 to resolve as the same type (probably any, but preferably unknown)
I expect AN0, AN1 and AN2 to resolve as the same type (probably never)

Additional information about the issue

No response

@rotu rotu changed the title Intersection, Union depends on type variable resolution Intersection, Union depends on type variable instantiation order Dec 30, 2023
@rotu
Copy link
Author

rotu commented Dec 30, 2023

The issue may be that unknown is assumed to be an absorbing element for | and any an absorbing element for &. I think it's reasonable that a type expression containing type parameters should depend only on the eventual value of those type parameters.

@fatcerberus
Copy link

Generally speaking, both union and intersection type operators are commutative (with the exception of function intersections, which are treated as overloads and are order-dependent), and any union/intersection containing any should always reduce to any.

@rotu
Copy link
Author

rotu commented Dec 31, 2023

@fatcerberus gotcha.

any union/intersection containing any should always reduce to any.

I think it would be logically consistent for either:

  • any | unknown and any & never are both any, since any represents degeneracy of the type system.
  • T | unknown is unknown, T & never is never, since unknown and never bound the type hierarchy and any contains no information about the qualified value.

Why should the former be overriding? I was unable to find docs that make this clear, but it feels like an important design choice.

Edit: I can see that the intent was for unknown & any and unknown | any to both be any in #24439. But the documented behavior there differs from current, namely:

type T32<T> = never extends T ? true : false;  // true

which seems more consistent than the current behavior (where this type evaluates as never)

Edit again: my understanding current behavior for distributive conditional was wrong. It only distributes when the left-hand side is a type parameter.

@fatcerberus
Copy link

fatcerberus commented Dec 31, 2023

It only distributes when the left-hand side is a type parameter.

Specifically, a naked type parameter. Stuff like (T & U) extends ... or whatever is also not distributive. Which is why you can write [T] extends [U] ? ... to prevent distribution.

@rotu
Copy link
Author

rotu commented Dec 31, 2023

It only distributes when the left-hand side is a type parameter.

Specifically, a naked type parameter. Stuff like (T & U) extends ... or whatever is also not distributive.

Oy. You’re right. And I can’t figure out when that’s a bug or a feature.

For instance T|T behaves like the naked type parameter T but T|U does not, even when T and U are instantiated with the same type!

There’s also the curious case that even when the LHS does not contain a type parameter, when the LHS is any but the RHS is not, the conditional evaluates to the union of both branches.

@fatcerberus
Copy link

There’s also the curious case that even when the LHS does not contain a type parameter, when the LHS is any but the RHS is not, the conditional evaluates to the union of both branches.

Yeah, that’s a separate behavior from distribution and is intentional.

@rotu
Copy link
Author

rotu commented Dec 31, 2023

There’s also the curious case that even when the LHS does not contain a type parameter, when the LHS is any but the RHS is not, the conditional evaluates to the union of both branches.

Yeah, that’s a separate behavior from distribution and is intentional.

Yes, it’s intentional. The reason for it (any is an upper bound of union, so we should infer the most general type possible) seems to imply that unknown extends T ? A : B should evaluate to A | B.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Jan 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants