Repository navigation
[...T[], T] does not extend [T, ...T[]] #60463
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
💻 Code
type Test = [...1[], 1] extends [1, ...1[]] ? true : false;
// ^? type Test = falseIn 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
jcalz commentedon Nov 9, 2024
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.Andarist commentedon Nov 10, 2024
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 :( )
geoffreytools commentedon Nov 11, 2024
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.appendElementwhich you can pair withArray.isNonEmptyReadonlyArrayandArray.lastNonEmptyand 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.
janvogt commentedon Aug 13, 2025
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:
What should work but doesn't:
The operation
[...xs, x]produces[...T[], T](guaranteed non-empty with last element of typeT), which should be assignable to[T, ...T[]](guaranteed non-empty with first element of typeT) since both represent the same constraint: a non-empty array where all elements are of typeT.This limitation forces us to use type assertions throughout our codebase for semantically safe operations, reducing type safety and code clarity.