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, []> // []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).
π Lifetime-License is leaving on August 10, 2026
Get it now for $29
One-time payment. Lifetime access to all pro challenges.
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.
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.
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':
Acc starts as [], whose 'length' is the literal 0.[...Acc, Head].Acc['length'] extends N becomes true the moment we've collected N items.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.
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.
Take<0, [1, 2, 3]> β []: the very first Acc['length'] extends 0 check is already true, so nothing is ever consumed.Take<5, [1, 2, 3]> β [1, 2, 3]: the "tuple exhausted" branch returns the partial accumulator instead of failing.Take<3, []> β []: same branch, degenerate input.Take<-2, [1, 2, 3]> β [2, 3]: sign detection routes to TakeLast, and prepending keeps the order intact.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.