Cut an even-length tuple into two halves at the type level. A growing accumulator and a shrinking tuple meet in the middle, so no arithmetic is needed.
Halving a tuple sounds like it needs division. It does not.
SplitTupleInto2<T> takes a tuple whose length is even, say 2n, and returns a pair: the first n elements in one tuple, the last n in the other. The type system has no / operator, so the interesting part is finding the midpoint without ever computing it.
For example
type A = SplitTupleInto2<[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]>
// [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]]
type B = SplitTupleInto2<['a', 'b', 'c', 'd', 'e', 'f']>
// [['a', 'b', 'c'], ['d', 'e', 'f']]Given a tuple whose length is even, let's say 2n.
Split it into 2 sub-tuples with first n elements in the first one, and last n elements in the last one.
For example
type A = SplitTupleInto2<[0,1,2,3,4, 5,6,7,8,9]>
//[[0,1,2,3,4], [5,6,7,8,9]]
type B = SplitTupleInto2<["a","b","c", "d","e","f"]>
//[["a","b","c"], ["d","e","f"]]View on GitHub: https://tsch.js.org/36870
Change the following code to make the test cases pass (no type check errors).
/* _____________ Your Code Here _____________ */
type SplitTupleInto2<T extends any[]> = any
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'
type cases = [
Expect<
Equal<SplitTupleInto2<[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]>, [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]]>
>,
Expect<
Equal<
SplitTupleInto2<['a', 'b', 'c', 'd', 'e', 'f']>,
[['a', 'b', 'c'], ['d', 'e', 'f']]
>
>,
Expect<Equal<SplitTupleInto2<[]>, [[], []]>>,
]
Unlock 170+ medium, hard, and extreme challenges to master advanced TypeScript.
Monthly subscription. Cancel anytime.
The solution in full:
type SplitTupleInto2<T extends any[], Left extends any[] = []> = Left['length'] extends T['length']
? [Left, T]
: T extends [infer Head, ...infer Tail]
? SplitTupleInto2<Tail, [...Left, Head]>
: neverTwo tuples move toward each other and the midpoint is wherever they meet.
Left is an extra type parameter with a default of []. Callers never pass it: SplitTupleInto2<[1, 2]> works because the default fills the slot. Each recursive step passes a slightly longer Left, which is how a type keeps state across recursion. This accumulator pattern shows up everywhere in type-level code, and here it doubles as the answer: when the recursion stops, Left already is the first half.
T extends [infer Head, ...infer Tail] peels the first element off the tuple. Head is bound to that element's type, Tail to everything after it, both with their literal types intact. The recursive call then hands Tail back as the new T and [...Left, Head] as the new Left, so the element that left the front of T arrives at the back of Left.
For T = ['a', 'b', 'c', 'd'], the state after each step looks like this:
// Left = [] T = ['a', 'b', 'c', 'd']
// Left = ['a'] T = ['b', 'c', 'd']
// Left = ['a', 'b'] T = ['c', 'd']Left['length'] on a tuple is not number, it is the exact numeric literal: ['a', 'b']['length'] is 2. Since Left gains one element per step while T loses one, the two lengths close a gap of 2n at a rate of two per step, and after n steps they are equal. That is exactly the midpoint, and Left['length'] extends T['length'] is the check that spots it.
At that point Left holds the first half and T holds what is left of the original, which is the second half. Returning [Left, T] needs no further work:
[object Object]The recursion terminates because T shrinks on every step. Even if the lengths never match, T eventually becomes [], the [infer Head, ...infer Tail] pattern stops matching, and the final never branch ends it.
Order matters here. The length comparison sits above the peeling step, so an already-balanced pair is returned before anything else is consumed. Flip the two and an empty input would fall straight through to never instead of producing a pair of empty tuples.
[]: both lengths are 0 on the very first evaluation, so the result is [[], []] without a single recursive step.[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]: five steps, and every numeric literal survives the round trip through infer. The result is [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]], not [number[], number[]].['a', 'b', 'c', 'd', 'e', 'f']: the same walk on string literals, confirming nothing in the type depends on the element type.Odd-length tuples are outside the stated contract, and the type reports that honestly: the lengths step past each other without ever matching, T runs empty, and the result is never rather than a lopsided guess.
This challenge is originally from here.
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges