#36870•Medium

Split Tuple Into 2

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

Challenge Instructions: Split Tuple Into 2

Medium

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

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

Pro Challenge

Unlock 170+ medium, hard, and extreme challenges to master advanced TypeScript.

Monthly subscription. Cancel anytime.

Detailed Explanation

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

Two tuples move toward each other and the midpoint is wherever they meet.

The accumulator parameter

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.

Moving one element per step

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

Knowing when to stop

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.

Why the check comes first

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.

Edge cases the tests cover

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.

Share this challenge

Related Challenges

Learn the Concepts

Become a TypeScript Pro

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

Or start solving right away: explore all TypeScript challenges