#34286Hard

Take Elements

Take<N, Arr> extracts the first N elements of a tuple, or the last N when N is negative. Accumulator counting and template literal sign detection do the work.

Take<N, Arr> returns the first N elements from a tuple Arr. If N is negative, it returns the last |N| elements instead. The type system has no arithmetic and no slice, so you count with accumulator tuples and detect the sign of a number through template literal inference. Both techniques show up constantly in type-level array problems.

For example,

type T0 = Take<2, [1, 2, 3]> // [1, 2]
type T1 = Take<3, ['1', 2, true, false]> // ['1', 2, true]
type T2 = Take<-2, [1, 2, 3]> // [2, 3]
type T3 = Take<0, [1, 2, 3]> // []
type T4 = Take<5, [1, 2, 3]> // [1, 2, 3]
type T5 = Take<3, []> // []

Challenge Instructions: Take Elements

Hard

Implement a type Take<N, Arr> that returns the first N elements from an array Arr. If N is negative, return the last |N| elements

For example,

type T0 = Take<2, [1, 2, 3]> // [1, 2]
type T1 = Take<3, ['1', 2, true, false]> // ['1', 2, true]
type T2 = Take<-2, [1, 2, 3]> // [2, 3]
type T3 = Take<0, [1, 2, 3]> // []
type T4 = Take<5, [1, 2, 3]> // [1, 2, 3]
type T5 = Take<3, []> // []

View on GitHub: https://tsch.js.org/34286

Change the following code to make the test cases pass (no type check errors).

ChallengeSolution
/* _____________ Your Code Here _____________ */

type Take<N, Arr> = Arr

/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'

type cases = [
  Expect<Equal<Take<2, [1, 2, 3]>, [1, 2]>>,
  Expect<Equal<Take<3, ['1', 2, true, false]>, ['1', 2, true]>>,
  Expect<Equal<Take<-2, [1, 2, 3]>, [2, 3]>>,
  Expect<Equal<Take<0, [1, 2, 3]>, []>>,
  Expect<Equal<Take<5, [1, 2, 3]>, [1, 2, 3]>>,
  Expect<Equal<Take<3, []>, []>>,
]

Pro Challenge

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

Monthly subscription. Cancel anytime.

Detailed Explanation

The solution, all three types:

type TakeFirst<
  N extends number,
  Arr extends unknown[],
  Acc extends unknown[] = [],
> = Acc['length'] extends N
  ? Acc
  : Arr extends [infer Head, ...infer Rest]
    ? TakeFirst<N, Rest, [...Acc, Head]>
    : Acc
 
type TakeLast<
  N extends number,
  Arr extends unknown[],
  Acc extends unknown[] = [],
> = Acc['length'] extends N
  ? Acc
  : Arr extends [...infer Rest, infer Last]
    ? TakeLast<N, Rest, [Last, ...Acc]>
    : Acc
 
type Take<N extends number, Arr extends unknown[]> =
  `${N}` extends `-${infer P extends number}`
    ? TakeLast<P, Arr>
    : TakeFirst<N, Arr>

The problem splits into taking from the front, taking from the back, and deciding which of the two applies. Start with the decision.

Detecting a negative number

Types can't do N < 0, but they can look at how a number prints. Interpolating a numeric literal into a template literal turns it into a string literal: `${-2}` is '-2'. So the check

[object Object]

asks: does N start with a minus sign when written out? If it does, infer P extends number captures the digits after the minus and converts them back into a number. For N = -2, P becomes 2. That extends number inside an infer clause is a TypeScript 4.8+ feature; without it P would stay a string like '2', which is useless for counting. This one line gives us both the sign check and the absolute value in a single pattern match.

Counting without arithmetic: the accumulator

TakeFirst needs to stop after exactly N elements, but there's no N - 1 in the type system. The standard trick is to grow a tuple and read its 'length':

Watch it run for Take<2, [1, 2, 3]>:

// TakeFirst<2, [1, 2, 3], []>       → length 0, keep going
// TakeFirst<2, [2, 3],    [1]>      → length 1, keep going
// TakeFirst<2, [3],       [1, 2]>   → length 2 extends 2 → return [1, 2]

The second exit condition is just as important: when Arr no longer matches [infer Head, ...infer Rest], the input is exhausted and we return whatever we have. That's what makes Take<5, [1, 2, 3]> gracefully return [1, 2, 3] and Take<3, []> return [] instead of recursing forever.

Taking from the back

TakeLast is the mirror image. Variadic tuple types let you match from the end just as easily as from the front: [...infer Rest, infer Last] peels off the final element. Each peeled element is prepended to the accumulator, [Last, ...Acc], so the original order is preserved. For Take<-2, [1, 2, 3]>:

// TakeLast<2, [1, 2, 3], []>     → length 0 ≠ 2 → peel 3 → Acc = [3]
// TakeLast<2, [1, 2],    [3]>    → length 1 ≠ 2 → peel 2 → Acc = [2, 3]
// TakeLast<2, [1],       [2, 3]> → length 2 extends 2 → return [2, 3]

If we had appended instead of prepended, the result would come out reversed as [3, 2], an easy mistake to make in these recursions.

Edge cases the tests cover

The pattern of "recurse while growing an accumulator, compare its 'length' to stop" is the type-level equivalent of a counting loop. Once you've written it here, you'll recognize it in dozens of other tuple challenges.

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