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:

  1. the name of a toy

  2. 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 current Toy to the beginning of the accumulator Tuple.

  • The base case is when the length of the accumulator Tuple becomes equal to Boxes. 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 if Boxes extends any (which is always true). If true, it uses the Repeat helper type to generate the tuple.

  • If Boxes is a single number, it generates a tuple of Toy repeated Boxes times. If Boxes 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 is never.

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:

  1. For 2:

    • Repeat<'example', 2> generates a tuple ['example', 'example'].
  2. For 3:

    • Repeat<'example', 3> generates a tuple ['example', 'example', 'example'].
  3. Union of the Results:

    • The distributive property combines the individual results into a union: ['example', 'example'] | ['example', 'example', 'example'].

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 🐦