Skip to content

Implicit type conversion of tuple [...T[]] to array T[] #29311

Description

TypeScript Version: 3.3.0-dev.20190108

Search Terms: spread, rest, type conversion, tuple, array length

Code

type test1 = [...number[]]           // number[]     <-- Is it a feature?
type test2 = [number, ...number[]]   // [number, ...number[]]
type test3 = [number?, ...number[]]  // [number?, ...number[]]

type fixed1 = test1 & { length: 2 }  // number[] & { length: 2 }
type fixed2 = test2 & { length: 2 }  // [number, ...number[]] & { length: 2 }
type fixed3 = test3 & { length: 2 }  // [number?, ...number[]] & { length: 2 }

let var1: fixed1 = [0, 0]
/*  error TS2322: Type 'number[]' is not assignable to type 'fixed1'.
    Type 'number[]' is not assignable to type '{ length: 2; }'.
        Types of property 'length' are incompatible.
        Type 'number' is not assignable to type '2'. */
let var2: fixed2 = [0, 0] // OK
let var3: fixed3 = [0, 0] // OK

Expected behaviour:
Expected [...number[]] and [number, ...number[]] to have the same behaviour except that the latter requires at least one element in tuple.

Actual behavior:
[...number[]] is implicitly converted to number[] with the length: number field, that is incompatible with length: 2

Error TS2322: Type 'number[]' is not assignable to type 'fixed1'.
    Type 'number[]' is not assignable to type '{ length: 2; }'.
        Types of property 'length' are incompatible.
        Type 'number' is not assignable to type '2'.

Playground Link: 🔗

Related Issues: Haven't found anything about the same problem.

Activity

  1. weswigham commented on Jan 8, 2019

    @weswigham
    Member

    In theory they're the same, but in practice the simplification we do deletes the fact that the type should be a tuple at all, which causes the error. So, because of that, it might be best not to perform the inlining so as to preserve the tupleness of the type.

  2. ahejlsberg commented on Jan 9, 2019

    @ahejlsberg
    Member

    We definitely want to keep the simplification of [...T[]] to T[]. To fix the issue we should instead consider types that are array-like and have a length property of a numeric literal type to be tuple-like. That would cause the contextual type to be tuple-like so we'd infer a tuple type for [0, 0].

  3. weswigham commented on Jan 11, 2019

    @weswigham
    Member

    To fix the issue we should instead consider types that are array-like and have a length property of a numeric literal type to be tuple-like

    And/or having numeric-named members (eg, 0: Thing), as is our internal tuplish criteria?

  4. Andarist commented on Feb 4, 2023

    @Andarist
    Contributor

    The simplification at hand makes it hard to create a tuple constraint:

    declare function test<T extends [...any[]]>(a: T): T
    
    const res1 = test([1, 2]) 
    //        ^? actual: number[], expected: [number, number]

    There are different ways of dealing with this:

    • the old weird trick of T[] | [T]
    • [T, ...T[]] - it doesn't allow an empty tuple
    • [T?, ...T[]] - it might look OK at first glance but when passing T[K] through a mapped type to some other type, it will assume that it might be undefined too, so this solution has some unintended caveats
    • const type params in 5.0+ - this implies constness which isn't always desirable
    • using the spread at argument position: declare function test<T extends any[]>(a: [...T]): T. This works exactly as expected but requires moving the "hint" to the argument position

    It seems that the most "complete" way of defining a tuple constraint using tuple types is... [T, ...T[]] | []. It's quite cumbersome.

  5. locked as resolved and limited conversation to collaborators on Oct 21, 2025
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

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions