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 | 8Create 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 | 8View on GitHub: https://tsch.js.org/37374
Change the following code to make the test cases pass (no type check errors).
/* _____________ 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>>,
]
Unlock 170+ medium, hard, and extreme challenges to master advanced TypeScript.
Monthly subscription. Cancel anytime.
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.
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'] // 3That 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.
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 | 3Start with Keep as false and the first number you accept is 1. Start with true and you accept 0, 2, 4 and so on.
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.
OddNumbers<21> includes 21 and EvensNumbers<16> includes 16, because the stop condition fires after the final number has been contributed, not before.OddNumbers<16> stops at 15 and EvensNumbers<21> stops at 20. The bound itself is reached, but the flag is off when it is, so it contributes never.1 | 3 | 5 and 5 | 3 | 1 are the same type to Equal. The order the walker emits them in does not matter.OddNumbers<0> is never and EvensNumbers<0> is never as well, since 0 is the one even number Exclude removes.OddNumbers<45> still resolves, while OddNumbers<50> reports TS2589. Reaching into the hundreds needs a different shape, such as a tail-recursive accumulator that builds a tuple of results and turns it into a union at the end.This challenge is originally from here.
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges