Skip to content

[...T[], T] does not extend [T, ...T[]] #60463

Open

Description

🔎 Search Terms

homogeneous tuple, non-empty array, assert not empty

🕗 Version & Regression Information

This is the behavior in every version I tried, and I reviewed the FAQ for entries about array/tuple

⏯ Playground Link

https://www.typescriptlang.org/play/?jsx=0&ts=5.7.0-beta#code/C4TwDgpgBAKhDOwoF4oG0B0WCMaC6ANFNnlBAB7AQB2AJvOtkVhrnqQPxTABOArtABcUAGYBDADbwIAbgBQAegVQVAPQ5zNAYwD21RFACW8AKIBbMKBRQAPDAB8ACnLxhMfAEphLow3wp7KABCFwwJGgBzYAALOV19JAkxA1Q7JxdhNBhmLHd2DwCoFzRQ8Ooo6KgAWmI8eTlQSCgAMR0da38AH3RqPjMAIwgeHIxegaH8OrlaCC0knmh4gwyWtvkZubEFqCWkEFcoMcGefE1DEUcg43NLEGd4DwKAbzkVFV2oAC8D1vbUTCw+yISUQ9w8pGSOz0iHkAF8gA

💻 Code

type Test = [...1[], 1] extends [1, ...1[]] ? true : false;
//   ^? type Test = false

In context:

const isEmpty = <T>(xs: T[]): xs is [] => !xs.length
const last = <T>(xs: [T, ...T[]]) => xs[xs.length - 1];

type Foo = [] | [number, ...number[]];
declare const xs: Foo;
declare const ys: number[]

if(!isEmpty(xs)) {
    const zs: Foo = [...ys, last(xs)] as const;
//        ~~
// Type '[...number[], number]' is not assignable to type 'Foo'.
//  Type '[...number[], number]' is not assignable to type '[number, ...number[]]'.
//    Source provides no match for required element at position 0 in target.(2322)
}

🙁 Actual behavior

For a homogeneous tuple, [...T[], T] and [T, ...T[]] are the same thing.

TS fails to recognise that in the in-context example: it is saying that [...ys, last(xs)] could be missing a required number but it can't because last(xs) is the required number that it is looking for. The only issue is that it is of type [...number[], number] and since [...T[], U] is not the same thing as [T, ...U[]], It chokes.

🙂 Expected behavior

Test should be true and zs: Foo should not be a type error.

Additional information about the issue

I wish !isEmpty could narrow T[] to [T, ...T[]].

Using a isNotEmpty helper with Foo defined as number[] would solve this specific case but in the wild it would force me to call it like so !isNotEmpty(xs) in many places, which is terrible for readability.

It would be possible in the in-context example to define Foo as [] | [number, ...number[]] | [...number[], number] and last as <T>(xs: [T, ...T[]] | [...T[], T]) =>T but nobody in the world is doing that and I would be splitting hair alone in my basement not being able to use any library.

I think I trip over this more often in recursive logic.

Activity

  1. jcalz commented on Nov 9, 2024

    @jcalz
    Contributor

    Ugh I wonder if there's any way to make TS aware of this kind of equivalence without a big performance hit. Maybe in the case where you have a tuple type where a rest element is followed by another element of the identical type, TS could just normalize it by swapping them? So if you wrote type Foo = [0, ...0[], 0, 0, 1] it would get normalized to [0, 0, 0, ...0[], 1]? Would that even be worth the extra work? How often does this sort of thing show up in real-world code? When leading/middle rest elements were introduced in #41544 I had a creeping feeling things like this might show up, but so far it hasn't seemed to be an issue at all... well, until now.

  2. Andarist commented on Nov 10, 2024

    @Andarist
    Contributor

    I recall this being pointed out at least once before and, IIRC, even Anders commented on it that something could be done about it in theory but ROI sounds rather low (I can’t find that comment though :( )

  3. geoffreytools commented on Nov 11, 2024

    @geoffreytools
    Author

    I am not surprised that this is not mentioned so often.

    I do encounter this more in recursive situations (including Array.reduce) in which you build a data-structure, because you must return the thing you started with, so any quirk or loss of type information is punitive. These situations are naturally not encountered by developers who prefer to solve problems with iteration, mutations and type annotations; and I would take the bet that some teams ban recursive solutions for fear that they would become a time sink to maintain. I do censor myself sometimes: I start with a recursive solution, then identify that it's going to be brittle type-wise and bailout.

    Additionally, in the functional world you learn not to rely on native code to maintain this kind of invariants. Instead of using the spread syntax you will use something like effect/Tuple.appendElement which you can pair with Array.isNonEmptyReadonlyArray and Array.lastNonEmpty and you now have a very verbose import section with lots of aliases; Alternatively, you can use functional data-structures and play with them in your basement with your imaginary friend ;)

    Situations which are challenging for the type checker and the inference engine are naturally avoided by developers but I think it's a nice goal to try and make plain JavaScript immutable solutions compile because they can be very readable.

    I do get that some features are more costly to implement than others though.

  4. janvogt commented on Aug 13, 2025

    @janvogt

    I encounter this issue frequently as well. The type NonEmpty<T> = [T, ...T[]] pattern is widely used in our codebase to guarantee non-emptiness in our data models.

    For example, when implementing a simple function to cycle through a non-empty array, even a basic operation requires an unnecessary type assertion:

    Current workaround needed:

    function cycle<T>([x, ...xs]: NonEmpty<T>): NonEmpty<T> {
      return [...xs, x] as unknown as NonEmpty<T>;
    }

    What should work but doesn't:

    function cycle<T>([x, ...xs]: NonEmpty<T>): NonEmpty<T> {
      return [...xs, x]; // Error: Type '[...T[], T]' is not assignable to type '[T, ...T[]]'
    }

    The operation [...xs, x] produces [...T[], T] (guaranteed non-empty with last element of type T), which should be assignable to [T, ...T[]] (guaranteed non-empty with first element of type T) since both represent the same constraint: a non-empty array where all elements are of type T.

    This limitation forces us to use type assertions throughout our codebase for semantically safe operations, reducing type safety and code clarity.

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

    Domain: check: Type InferenceRelated to type inference performed during signature resolution or `infer` type resolutionHelp WantedYou can do thisPossible ImprovementThe current behavior isn't wrong, but it's possible to see that it might be better in some cases

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions