#36891•Hard

Split Tuple Into 3

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]]

Challenge Instructions: Split Tuple Into 3

Hard

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).

ChallengeSolution
/* _____________ 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'], [nev

Pro Challenge

Get access to all 200+ challenges, including every medium, hard, and extreme one.

One-time payment, lifetime access. 30-day money-back guarantee.

Detailed Explanation

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
    : never

Instead of walking the tuple once and cutting it twice, the recursion eats it from both ends and lets the middle third be whatever survives.

Two accumulators instead of one

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.

Peeling off the front and the back in one step

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   = 6

The recursive call then uses Rest as the new T, so each step removes two elements, one from each side.

Keeping the original order

[...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.

Knowing when to stop

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]]

Why the check comes first

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.

What happens to a bad length

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.

Edge cases the tests cover

This challenge is originally from here.

Share this challenge

Related Challenges

Learn the Concepts

Become a TypeScript Pro

Track your progress through 200+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges