Challenge

Sometimes we want to limit the range of numbers… For example:

type result = NumberRange<2, 9>; // | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

Solution

I love challenges related to the arithmetic and hate them at the same time. They are challenging and built on the workarounds. I love them for being challenging and hate them for being built on workarounds.

Anyway, let’s start with the numbers. We need to get a union of numbers, specific numbers. To get the union of numbers, we simply can use the lookup types. For instance, having a tuple with range 0-5 and using a lookup type for number type we can get the union:

type R0 = [0, 1, 2, 3, 4, 5][number];
// R0 is 0 | 1 | 2 | 3 | 4 | 5

Meaning, we can solve the challenge if we have a tuple with the needed numbers inside. How to create one?

We can start by creating a tuple of specific length. Let’s call the type that creates it Tuple. The type will have a single type parameter L that we can use to specify the length of the tuple:

type Tuple<L extends number> = any;

For instance, we want to create a tuple of length 2. So our type parameter L will be 2. What needs to be compared with it?

When having a tuple, we can use lookup types to get the property length. It will return the length of a tuple as a number. And if the length of a tuple is equal to type parameter L – we have one:

type Tuple<L extends number> = A["length"] extends L ? never : never;

Let’s add a type parameter A (Accumulator) to the definition and by default make it an empty one to fix the compilation error about type parameter A being not defined:

type Tuple<L extends number, A extends never[] = []> = A["length"] extends L
  ? never
  : never;

Now, having an accumulator with length of 0 and the required length of 2, we don’t pass the condition. In such case, we call ourselves recursively, but pushing the element into the accumulator, until the length of accumulator matches the required length:

type Tuple<L extends number, A extends never[] = []> = A["length"] extends L
  ? never
  : Tuple<L, [...A, never]>;

Once we get the needed length of an accumulator, we will pass the conditional type and can return it:

type Tuple<L extends number, A extends never[] = []> = A["length"] extends L
  ? A
  : Tuple<L, [...A, never]>;

Using the type is pretty simple. For instance, passing 5 as a length of a tuple, we get 5 never-s:

type R0 = Tuple<5>;
// R0 is [never, never, never, never, never]

We have a type that creates a tuple of required length, filled with never type. Now, back to the challenge.

There is a low (L) and high (H) parameters that specify the minimum and maximum of a range:

type NumberRange<L, H> = any;

Creating a tuple of the length L, filled with never-s, will give us a range 0-L which is placed into another accumulator A by default:

type NumberRange<
  L extends number,
  H extends number,
  A extends number[] = Tuple<L>,
> = any;

Sitting in the position of L now, we need to start filling the tuple with the actual numbers we will use for a union later. Since our values in the tuple matches the indexes they have, we can simply use the property length as a value:

type NumberRange<
  L extends number,
  H extends number,
  A extends number[] = Tuple<L>,
> = [...A, A["length"]];

So we got all the never-s until the L, and now we are getting the actual numbers from L and greater. This needs to be repeated recursively till we get to the H position. So we check if our accumulator length is equal to H and if not – recursion:

type NumberRange<
  L extends number,
  H extends number,
  A extends number[] = Tuple<L>,
> = A["length"] extends H ? never : NumberRange<L, H, [...A, A["length"]]>;

At this point, we have a tuple of never types till the position of L and actual numbers till the position of H. The only thing left is to return the built accumulator in the case of length matches the H:

type NumberRange<
  L extends number,
  H extends number,
  A extends number[] = Tuple<L>,
> = A["length"] extends H ? A : NumberRange<L, H, [...A, A["length"]]>;

However, the accumulator does not include the last item. So we add the length value to the tuple as well:

type NumberRange<
  L extends number,
  H extends number,
  A extends number[] = Tuple<L>,
> = A["length"] extends H
  ? [...A, A["length"]]
  : NumberRange<L, H, [...A, A["length"]]>;

At this point, we have the tuple we need. It has the range of never type from 0 to L and the numbers from L to H. never type will be ignored in union, so we don’t care about it. The only thing what’s left is to use the lookup type with the number type and get the union:

type NumberRange<
  L extends number,
  H extends number,
  A extends number[] = Tuple<L>,
> = A["length"] extends H
  ? [...A, A["length"]][number]
  : NumberRange<L, H, [...A, A["length"]]>;

The whole solution, including the type Tuple:

type Tuple<L extends number, A extends never[] = []> = A["length"] extends L
  ? A
  : Tuple<L, [...A, never]>;

type NumberRange<
  L extends number,
  H extends number,
  A extends number[] = Tuple<L>,
> = A["length"] extends H
  ? [...A, A["length"]][number]
  : NumberRange<L, H, [...A, A["length"]]>;

I know, it’s hard to grasp all of it at first. Take your time, re-read it several times, follow the code, and you will understand it in no time.

References