Skip to content

Intersection, Union depends on type variable instantiation order #56906

@rotu

Description

@rotu

🔎 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

Activity

changed the title [-]Intersection, Union depends on type variable resolution[/-] [+]Intersection, Union depends on type variable instantiation order[/+] on Dec 30, 2023
rotu

rotu commented on Dec 30, 2023

@rotu
Author

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

fatcerberus commented on Dec 30, 2023

@fatcerberus

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

rotu commented on Dec 31, 2023

@rotu
Author

@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

fatcerberus commented on Dec 31, 2023

@fatcerberus

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

rotu commented on Dec 31, 2023

@rotu
Author

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

fatcerberus commented on Dec 31, 2023

@fatcerberus

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

rotu commented on Dec 31, 2023

@rotu
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @rotu@fatcerberus@RyanCavanaugh

        Issue actions

          Intersection, Union depends on type variable instantiation order · Issue #56906 · microsoft/TypeScript