Skip to content

Variadic element of tuple-like intersection types are spreading incorrectly #40945

Open

Description

TypeScript Version: 4.0.3

Search Terms: variadic tuple, variadic element, intersection type, tuple-like type

Code

type T1 = [...unknown[] & []]; // never[]
type T2 = [...number[] & [1, 2, 3]]; // (3 | 1 | 2)[]

Expected behavior: for T1, I expected it to be [](empty tuple type) and for T2, I expected it to be [1, 2, 3].

Actual behavior: Both T1 and T2 became Array rather than tuple somehow. (T1 is deduced to never[], T2 is deduced to (3 | 1 | 2)[])

detailed explanation

In some perspective, unknown could be recognized as type of all types. Therefore, [unknown] is type of all type-level 1-tuples and intuitively, unknown[] is type of all type-level n-tuples.

So [] <: unknown[] is true (i.e. all empty type-level tuple is subtype of all type-level n-tuples) and you can check it simply with this code:

type M = [] & unknown[] extends [] ? [] extends [] & unknown[] ? true : false : false; // true

However, in spite of the fact that [] is subtype of unknown[] therefore [] & unknown[] is actually [], variadic element of [] & unknown[] somehow spreads as never[], which is incorrect and unintuitive.

Playground Link: Playground

Activity

  1. andrewbranch commented on Oct 7, 2020

    @andrewbranch
    Member

    incorrect and unintuitive

    Unintuitive, yes; not as good as we could do, yes; but “incorrect” is somewhat debatable. (As an aside, this issue led to a great conversation about whether never[] is conceptually the same as [], despite specific differences in their assignability.) At any rate, since never[] is not assignable to [], that’s a good argument for T1 being “incorrect,” but (3 | 1 | 2)[] is clearly a valid representation of [1, 2, 3]; it’s just not a very good one 😄

    Out of curiosity, since there are 4 👍 on an issue that seems somewhat arcane, is this coming up in practice somewhere?

  2. ENvironmentSet commented on Oct 7, 2020

    @ENvironmentSet
    Author

    I was building type-level programming library since TS 4.1 finally added recursive conditional type alias. But after few hours, I encountered this issue and talked with others in twitter for a few days. That 👍seems to be added by people I know or discussed with me about this.

  3. ENvironmentSet commented on Oct 8, 2020

    @ENvironmentSet
    Author

    I agree on that T1 is not being incorrect, sorry for misusing word. However, I think we all agree on that ( 3 | 2 | 1 )[] is not a proper or useful typing at all at this point since some important informations are discarded and it might cause some unexpected problems. 🙂

    (Sorry for loose formatting, I wrote this comment with my mobile phone...)

  4. added
    BugA bug in TypeScript
    Effort: ModerateRequires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual".
    on Oct 8, 2020
  5. sethveale commented on Oct 23, 2020

    @sethveale

    I was building type-level programming library since TS 4.1 finally added recursive conditional type alias. But after few hours, I encountered this issue and talked with others in twitter for a few days. That 👍seems to be added by people I know or discussed with me about this.

    Similar scenario for me. I was trying to map nested tuple types.

  6. mkantor commented on Mar 11, 2022

    @mkantor
    Contributor

    This also becomes an array type, even though both sides of the intersection were tuples to begin with:

    type X = [...[1, 2] & [1, 2 | 3]]
    //   ^? type X = (2 | 1)[]
  7. geoffreytools commented on Mar 11, 2022

    @geoffreytools

    Actually, this is also broken [...[1] & [unknown]]

    I had a similar "issue" when trying to get a tuple back from a tuple augmented with other properties.

    Investigation led me to the discovery that even [...([1,2,3] & unknown[])] was lossy

    You will find bellow my different attempts at getting the tuple back. Explicitly iterating over the numeric keys worked but I was surprised I had to go this far.

    type A = [1,2,3] & { foo: unknown };
    
    // -> (3 | 1 | 2)[]
    type B = [...A]
    
    // -> { [x: number]: 3 | 1 | 2 }
    type C = { [K in keyof A as K extends number ? K : never]: A[K] }
    
    // -> [2 | 1 | 3]
    type D = CloneTuple<A>
    
    type CloneTuple<T extends unknown[], R extends unknown[]=[]> =
        T extends [infer A, ...infer Rest]
        ? CloneTuple<Rest, [...R, A]>
        : R
    
    // -> [1, 2, 3]
    type E = ToTuple<A>
    
    type ToTuple<
        T extends unknown[],
        L extends number=T['length'],
        C extends number = 0,
        R extends unknown[]=[]
    > = C extends L ? R : ToTuple<
        T,
        L,
        [1,2,3,4,5,6,7,8,9,10][C],
        [...R, T[C]]
    >
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

    BugA bug in TypeScriptDomain: IntersectionIncorrect behavior under `T & U` constructs

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions