#37374•Medium

Odd and Even

Build the union 1 | 3 | 5 up to a bound using only type-level recursion. A tuple grows one element per step and its length is the number you are currently on.

The type system has no + operator, so counting to 21 takes a detour.

OddNumbers<T> produces a union of every odd number from 1 up to and including T, and EvensNumbers<T> does the same for the even numbers starting at 2. Both take the bound as a type argument, so the work is counting: you have to walk 0, 1, 2, 3 and so on, decide at each step whether the number belongs in the result, and stop when you reach T.

type Odd = OddNumbers<9> // 1 | 3 | 5 | 7 | 9
type Even = EvensNumbers<9> // 2 | 4 | 6 | 8

Challenge Instructions: Odd and Even

Medium

Create an OddNumbers type that generates the odd numbers from 1 up to the number you pass in.

Create an EvensNumbers type that generates the even numbers from 2 up to the number you pass in.

For example:

type Odd = OddNumbers<9> // expected to be 1 | 3 | 5 | 7 | 9
type Even = EvensNumbers<9> // expected to be 2 | 4 | 6 | 8

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

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

ChallengeSolution
/* _____________ Your Code Here _____________ */

type OddNumbers<T extends number> = any
type EvensNumbers<T extends number> = any

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

type cases = [
  Expect<Equal<OddNumbers<21>, 1 | 3 | 5 | 7 | 9 | 11 | 13 | 15 | 17 | 19 | 21>>,
  Expect<Equal<OddNumbers<16>, 1 | 3 | 5 | 7 | 9 | 11 | 13 | 15>>,
  Expect<Equal<EvensNumbers<16>, 2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 >>,
  Expect<Equal<EvensNumbers<21>, 2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 20>>,
]

Pro Challenge

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

Monthly subscription. Cancel anytime.

Detailed Explanation

The solution in full:

type CollectByParity<
  T extends number,
  Keep extends boolean,
  Counter extends unknown[] = [],
> = Counter['length'] extends T
  ? Keep extends true
    ? Counter['length']
    : never
  :
      | (Keep extends true ? Counter['length'] : never)
      | CollectByParity<T, Keep extends true ? false : true, [...Counter, unknown]>
 
type OddNumbers<T extends number> = CollectByParity<T, false>
type EvensNumbers<T extends number> = Exclude<CollectByParity<T, true>, 0>

One walker does both jobs. The only difference between odd and even is where the walker starts saying yes.

Counting with a tuple

You cannot add 1 to a numeric literal type, but you can add an element to a tuple type, and a tuple knows its own length as a literal:

type Zero = []['length'] // 0
type Three = [unknown, unknown, unknown]['length'] // 3

That gives you a counter. [...Counter, unknown] is the increment, and Counter['length'] reads the current value. The element type is irrelevant here, only the count matters, so unknown is as good a filler as any.

The stop condition is Counter['length'] extends T. There is no === at the type level, and for two numeric literals extends behaves the way you want: 21 extends 21 is true, 20 extends 21 is false.

Keeping every second number

Keep is a flag that says whether the number the counter currently holds belongs in the result. Each recursive step flips it with Keep extends true ? false : true, so the walker alternates yes, no, yes, no forever, and the starting value decides which half you get.

The result is assembled with a union rather than a tuple. The recursive branch contributes the current number, or never when the flag is off, and unions the rest of the walk onto it:

[object Object]

never is the empty union, so a skipped step contributes nothing and disappears on its own. No filtering pass is needed at the end.

Trace OddNumbers<3>, which is CollectByParity<3, false>:

// Counter []        length 0, Keep false -> never  | rest
// Counter [u]       length 1, Keep true  -> 1      | rest
// Counter [u,u]     length 2, Keep false -> never  | rest
// Counter [u,u,u]   length 3 extends 3, Keep true  -> 3, stop
// never | 1 | never | 3  ->  1 | 3

Start with Keep as false and the first number you accept is 1. Start with true and you accept 0, 2, 4 and so on.

Dropping the zero

Counting always begins at 0, and 0 is even, so CollectByParity<T, true> hands back 0 | 2 | 4 | .... The tests want the evens to begin at 2, so the last step removes one member:

[object Object]

Exclude is worth preferring over the alternative of starting the counter at [unknown, unknown]. A pre-filled counter never meets Counter['length'] extends T for T below 2, and the walk runs until the compiler gives up. Starting empty means every non-negative bound terminates.

Edge cases the tests cover

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