Photo by Liubomyr Vovchak on Unsplash
Understanding Conditional Types with Union Operations in TypeScript
We are getting close to our last Christmas challenge, in the meantime we are still pumping those TypeScript muscles.
In today's challenge, we will learn how conditional types allow us to leverage union types for repeated operations. Which is a very interesting and handy TypeScript feature.
Let's get into it!
Day fifteen - Dec 15
I strongly encourage you to try the first challenge on your own before reading the solution here.
Challenge: Box The Toys!
The challenge
[Santa walks by as Bernard, the head elf, is yelling at the other elves..]
[Bernard (to his staff)] LET'S GO ELVES! LET'S GO! KEEP BOXING TOYS!
[Santa] Bernard.. Seems like it's not going well.
[Bernard] Was anyone asking you!?
[Santa] Did you deploy the new toy boxing API yesterday?
[Bernard] No, we didn't get to it. Julius called out sick.
[Santa] Taking too many sick days shows a lack of commitment. We should get rid of Julius.
[Bernard (rolling eyes)] And then not replace him? Yeah. No Thanks.
[Santa] Well it was on the sprint and today's the last day of the sprint.
[Bernard] We don't deploy on Fridays.
[Santa] Aren't we doing continuous deployment now? You had this whole big thing at the last shareholder meeting about it?
[Bernard] No. For the 100th time. We're doing continuous delivery, which is completely different and gives us control over when we deploy.
[Santa] Well I need that
BoxToys
type. If you can't handle this project, Bernard, there are plenty of other elves who can. I need your full commitment.[Bernard] Ok. Fine. I'll do it myself.
[Santa] That's what I like to see!
The BoxToys API
The BoxToys
type takes two arguments:
the name of a toy
the number of of boxes we need for this toy
And the type will return a tuple containing that toy that number of times.
But there's one little thing.. We need to support the number of boxes being a union. That means our resulting tuple can also be a union. Check out test_nutcracker
in the tests to see how that works.
The code to complete
type BoxToys = unknown;
The tests
import { Expect, Equal } from 'type-testing';
type test_doll_actual = BoxToys<'doll', 1>;
type test_doll_expected = ['doll'];
type test_doll = Expect<Equal<test_doll_expected, test_doll_actual>>;
type test_nutcracker_actual = BoxToys<'nutcracker', 3 | 4>;
type test_nutcracker_expected =
| ['nutcracker', 'nutcracker', 'nutcracker']
| ['nutcracker', 'nutcracker', 'nutcracker', 'nutcracker'];
type test_nutcracker = Expect<Equal<test_nutcracker_expected, test_nutcracker_actual>>;
The solution
// Helper to generate the tuples
type Repeat<Toy, Boxes extends number, Tuple extends unknown[] = []> =
Tuple['length'] extends Boxes
? Tuple
: Repeat<Toy, Boxes, [Toy, ...Tuple]>;
// The main BoxToys type
type BoxToys<Toy, Boxes extends number> =
Boxes extends any ? Repeat<Toy, Boxes> : never;
I know I know... you are thinking: "OMG dead code, not recursion again🤮!"
Hey, in my defense I didn't create these challenges, I'm just trying to help naughty kids solve them before Christmas so they can get their presents.
For this challenge solution, we are creating the Repeat
type helper. Let's start by breaking that part down.
Repat
params are a Toy
, the number of Boxes
and a Tuple
, which will be our "accumulator" tuple to store the Toy
types.
This helper is a recursive type that generates a tuple tuple of the Toy
type based on the number of Boxes
.
This is how it works:
The recursive part
[Toy, ...Tuple]
adds the currentToy
to the beginning of the accumulatorTuple
.The base case is when the length of the accumulator
Tuple
becomes equal toBoxes
. In that case, the recursion stops, and the result is the generated tuple (Tuple
).
Hopefully, by now this is a straightforward explanation to you since have implemented similar recursive solutions in the past. If not you can review how we have done it here and here.
Now let's dive into the main type BoxToys
:
type BoxToys<Toy, Boxes extends number> =
Boxes extends any ? Repeat<Toy, Boxes> : never;
It takes two parameters: The Toy
to put in the box (which can be of any
type) and the number of boxes or a union of numbers.
Then this is how this type works:
We are creating a conditional type
Boxes extends any ? Repeat<Toy, Boxes> : never
that checks ifBoxes
extendsany
(which is always true). If true, it uses theRepeat
helper type to generate the tuple.If
Boxes
is a single number, it generates a tuple ofToy
repeatedBoxes
times. IfBoxes
is a union of numbers, it handles each number in the union, producing a union of tuples (I'll explain this further in a bit).If
Boxes
is not a number or a union of numbers, the result isnever
.
The part that is tricky about this type is how is handle a number
vs a union
of numbers.
When you have a conditional type applied to a union of types, the conditional type is applied independently to each element of the union, and the result is a union of the individual applications.
Let's use an example to illustrate this better:
type BoxToys<Toy, Boxes extends number> =
Boxes extends any ? Repeat<Toy, Boxes> : never;
type ExampleType = BoxToys<'nintendo', 2 | 3>;
Here, Repeat<Toy, Boxes>
from BoxToys
is a conditional type applied to a union of numbers (2 | 3
). Then the distributed property works its magic ✨.
In conditional types, the distributive property refers to the behavior where a conditional type applied to a union of types is distributed or applied independently to each element of the union.
This results in a union of the individual applications of the Repeat
helper.
This is what's happening under the hood:
For
2
:Repeat<'example', 2>
generates a tuple['example', 'example']
.
For
3
:Repeat<'example', 3>
generates a tuple['example', 'example', 'example']
.
Union of the Results:
- The distributive property combines the individual results into a union:
['example', 'example'] | ['example', 'example', 'example']
.
- The distributive property combines the individual results into a union:
Pretty neat right?
Phew, this one was tricky. But hey we are here to level up our TypeScript skills. Hope you enjoyed today's challenge, see you in the next one!
If you like this content consider checking out what I post on Twitter/X 🐦