Chunk
Challenge
Do you know lodash
? Chunk
is a very useful function in it, now let’s
implement it. Chunk<T, N>
accepts two required type parameters, the T
must
be a tuple
, and the N
must be an integer >=1
. For instance:
type R0 = Chunk<[1, 2, 3], 2>; // expected to be [[1, 2], [3]]
type R1 = Chunk<[1, 2, 3], 4>; // expected to be [[1, 2, 3]]
type R2 = Chunk<[1, 2, 3], 1>; // expected to be [[1], [2], [3]]
Solution
This challenge was a hard nut. But in the end, I came up with a solution that is easy to understand, IMHO. We start with an initial type that declares the contract:
type Chunk<T, N> = any;
Since we need to accumulate chunks of the tuple, it seems reasonable to have an
optional type parameter A
that accumulates the chunk of size N
. By default,
the type parameter A
will be an empty tuple:
type Chunk<T, N, A extends unknown[] = []> = any;
Having an empty accumulator that we will use for a temporary chunk, we can start
splitting the T
into parts. The parts are the first element of the tuple and
the rest of it:
type Chunk<T, N, A extends unknown[] = []> = T extends [infer H, ...infer T]
? never
: never;
Having parts of the tuple T
, we can check if our accumulator has a required
size. To achieve that, we lookup the length
property on its type. It works,
because we have a generic constraint over the type parameter A
that says it is
a tuple.
type Chunk<T, N, A extends unknown[] = []> = T extends [infer H, ...infer T]
? A["length"] extends N
? never
: never
: never;
In case our accumulator is empty or has not enough items in it, we need to
continue slicing the T
until the accumulator has the required size. To do
that, we continue calling Chunk
type recursively with a new accumulator. In
this accumulator, we push the previous one from A
and the item H
from the
T
:
type Chunk<T, N, A extends unknown[] = []> = T extends [infer H, ...infer T]
? A["length"] extends N
? never
: Chunk<T, N, [...A, H]>
: never;
The recursive call continues until we got a case when the accumulator size has
the required N
. This is exactly the case when our accumulator A
has all the
elements in it with proper size. It is our first chunk that we need to store in
the result. So we return a new tuple with accumulator in it:
type Chunk<T, N, A extends unknown[] = []> = T extends [infer H, ...infer T]
? A["length"] extends N
? [A]
: Chunk<T, N, [...A, H]>
: never;
Doing so, we ignore the rest of the tuple T
. So we need to add another
recursive call to our result [A]
that will clear the accumulator and start
again the same process:
type Chunk<T, N, A extends unknown[] = []> = T extends [infer H, ...infer T]
? A["length"] extends N
? [A, Chunk<T, N>]
: Chunk<T, N, [...A, H]>
: never;
This recursive magic continues until we get the case when there are no more
elements in tuple T
. In such scenario, we just return whatever is left in our
accumulator. The reasoning for this is that we can have a case when the size of
accumulator is less than N
. So not returning the accumulator in such case
means losing the items.
type Chunk<T, N, A extends unknown[] = []> = T extends [infer H, ...infer T]
? A["length"] extends N
? [A, Chunk<T, N>]
: Chunk<T, N, [...A, H]>
: [A];
There is also another case when we lose the element of H
. It is the case when
we got the accumulator of needed size, but ignore the inferred H
. Our chunks
lose some elements and this is not ok. To fix that, we need to not forget about
H
element when having an accumulator of size N
:
type Chunk<T, N, A extends unknown[] = []> = T extends [infer H, ...infer T]
? A["length"] extends N
? [A, Chunk<[H, ...T], N>]
: Chunk<T, N, [...A, H]>
: [A];
The solution solves some cases, which is great. However, we have a case when the
recursive call to the Chunk
type returns the tuples in the tuple in the tuple
(because of recursive calls). To overcome that, let’s add a spread to our
Chunk<[H, ...T], N>
:
type Chunk<T, N, A extends unknown[] = []> = T extends [infer H, ...infer T]
? A["length"] extends N
? [A, ...Chunk<[H, ...T], N>]
: Chunk<T, N, [...A, H]>
: [A];
All the test cases are passed! Hooray… except the edge case with an empty tuple. This one is just an edge case and we can add the conditional type to check it. If the accumulator turns out to be empty in the basic case, we return an empty tuple. Otherwise, we return the accumulator itself as before:
type Chunk<T, N, A extends unknown[] = []> = T extends [infer H, ...infer T]
? A["length"] extends N
? [A, ...Chunk<[H, ...T], N>]
: Chunk<T, N, [...A, H]>
: A[number] extends never
? []
: [A];
That’s all we need to implement a lodash version of .chunk()
function in the
type system!
Comments