Cut a tuple of length 3n into three equal parts. Peeling one element off each end per step makes both cut points appear without any arithmetic.
SplitTupleInto3<T> takes a tuple whose length is 3n and returns three sub-tuples of n elements each, in the original order. Splitting a tuple in half only needs one cut point, and a single accumulator finds it. Three parts need two cut points at once, which is where this gets interesting.
For example
type A = SplitTupleInto3<[boolean, 123n, 'type-challenges']>
// [[boolean], [123n], ['type-challenges']]
type B = SplitTupleInto3<[1, 2, 3, 4, 5, 6]>
// [[1, 2], [3, 4], [5, 6]]Given a tuple which length is 3n.
Split it into 3 sub-tuples, both have n elements. The order of elements should be kept.
For example
type A = SplitTupleInto3<[boolean,123n,'type-challenges']>
//[[boolean], [123n], ['type-challenges']
type B = SplitTupleInto3<[1,2,3,4,5,6]>
//[[1,2], [3,4], [5,6]]View on GitHub: https://tsch.js.org/36891
Change the following code to make the test cases pass (no type check errors).
/* _____________ Your Code Here _____________ */
type SplitTupleInto3<T extends any[]> = any
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'
type cases = [
Expect<Equal<SplitTupleInto3<[]>, [[], [], []]>>,
Expect<
Equal<
SplitTupleInto3<[boolean, 123n, 'type-challenges']>,
[[boolean], [123n], ['type-challenges']]
>
>,
Expect<Equal<SplitTupleInto3<[1, 2, 3, 4, 5, 6]>, [[1, 2], [3, 4], [5, 6]]>>,
Expect<
Equal<
SplitTupleInto3<[1 | 2, 'a', string & boolean, { id: 0 }, any, unknown]>,
[[1 | 2, 'a'], [nevGet access to all 200+ challenges, including every medium, hard, and extreme one.
One-time payment, lifetime access. 30-day money-back guarantee.
The solution in full:
type SplitTupleInto3<
T extends any[],
Front extends any[] = [],
Back extends any[] = [],
> = Front['length'] extends T['length']
? [Front, T, Back]
: T extends [infer Head, ...infer Middle]
? Middle extends [...infer Rest, infer Last]
? SplitTupleInto3<Rest, [...Front, Head], [Last, ...Back]>
: never
: neverInstead of walking the tuple once and cutting it twice, the recursion eats it from both ends and lets the middle third be whatever survives.
Front and Back are extra type parameters with a default of []. Callers never pass them, the defaults fill the slots, and each recursive call hands down slightly fuller versions. That is how a type carries state across recursion, and here the state is the answer: when the recursion stops, Front is the first third and Back is the last third.
The middle third needs no accumulator at all. It is simply what remains of T.
Two patterns do the work. T extends [infer Head, ...infer Middle] binds Head to the first element and Middle to everything after it. The second pattern, Middle extends [...infer Rest, infer Last], is the mirror image: the variadic ...infer Rest may sit at the start of a tuple pattern just as well as at the end, so Last is bound to the final element and Rest to everything before it.
For T = [1, 2, 3, 4, 5, 6] the first step evaluates to:
// Head = 1
// Middle = [2, 3, 4, 5, 6]
// Rest = [2, 3, 4, 5]
// Last = 6The recursive call then uses Rest as the new T, so each step removes two elements, one from each side.
[...Front, Head] appends, because the front accumulator grows left to right. [Last, ...Back] prepends, because the back accumulator is being filled right to left: the first Last seen is the very last element of the input and has to stay in last position. Append to Back instead of prepending and you get the final third reversed, which is the single easiest way to get this challenge wrong.
Front['length'] on a tuple is the exact numeric literal, not number. After k steps Front holds k elements, Back holds k, and T holds 3n - 2k. The condition Front['length'] extends T['length'] fires when k = 3n - 2k, which solves to k = n. At that moment all three tuples hold exactly n elements, and returning [Front, T, Back] finishes the job.
Walking [1, 2, 3, 4, 5, 6] through it:
// Front = [] T = [1, 2, 3, 4, 5, 6] Back = []
// Front = [1] T = [2, 3, 4, 5] Back = [6]
// Front = [1, 2] T = [3, 4] Back = [5, 6]
// 2 extends 2 -> [[1, 2], [3, 4], [5, 6]]The length comparison sits above both peeling steps. That ordering is what makes the empty tuple work: 0 extends 0 matches on the very first evaluation and returns [[], [], []] before anything is consumed. Put the peeling first and [] would fall through to never.
Nothing in the type guards against a length that is not a multiple of three, and nothing has to. T loses two elements per step while Front gains one, so the two counts close a gap of 3n at three per step. If the gap is not divisible by three the counts step past each other without ever matching, T runs out, one of the two patterns stops matching, and the result is never. Both never branches are reachable: a four-element tuple empties T completely and fails the outer pattern, while a five-element tuple ends on a one-element T whose Middle is empty and fails the inner one.
[]: zero steps, [[], [], []] straight from the length check.[boolean, 123n, 'type-challenges']: one step, three single-element tuples. Mixed element kinds, including a bigint literal, pass through infer unchanged.[1 | 2, 'a', string & boolean, { id: 0 }, any, unknown]: the awkward types. string & boolean has already reduced to never before the split ever runs, so the expected middle third is [never, { id: 0 }]. A never element does not collapse the tuple around it, and any in the last third stays any rather than swallowing its neighbours.[], [[]] and [[], []]: the patterns match on the outer tuple only, never on what an element contains, so the nesting is carried across untouched and each group lands in its own third.This challenge is originally from here.
Track your progress through 200+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges