Type Transformations Workshop (9 exercises)

Using Generic Context to Avoid Distributive Conditional Types

We're going to have to break our current mental model of conditional types to understand why we have this issue with distributive conditional types.

For reference, here is the failing code from the problem:

As mentioned, there are two ways to solve this challenge.

Using a Generic

The distributed conditional types problem can be solved by using a generic.

Consider the solution code below:

We create a new GetAppleOrBanana that accepts T. If T extends apple or banana, return T. Otherwise, return never.

Then we update AppleOrBanana to be GetAppleOrBanana, passing in Fruit.

This technique will work even when changing the Fruit type to include other options.

It works because when you pull a union into a generic like T, the members of the union distribute across it.

In other words, T will become each individual member of the union.

Check Your Understanding

Read the following code, and think through what is happening with T:

This line of code checks if T is either "apple" or "banana", and will return it if it is.

Think of it as iterating through the members of the union type to find what matches.

Now compare that solution to the original problem:

In the problem code, we're considering the entire Fruit union itself.

Because "apple" | "banana" | "orange" is not the same as "apple" | "banana", the code will return never.

This issue can really catch you off-guard if you aren't aware of it!

Solving with infer

It's also possible to solve this problem without a generic context:

Here we create a conditional type with Fruit extends infer T.

This infers what's inside of Fruit, and treats it as an iterable because it is now within a generic context!

With T acting as the iterable, the individual members of Fruit are checked similarly to the solution above.


[0:00] The solution here is pretty weird. It means you need to break a little bit of your mental model when it comes to conditional types. There is a difference between a union type that's expressed as a type here and one that you can get via a generic, so one that you pass in via a type argument.
[0:21] Let's actually look at the second solution first. Inside here, we're basically saying we've got GetAppleOrBanana T. We say that if T extends apple or banana, then return T. Otherwise, return never. This means that we're able to extract apple or banana from inside Fruit there.

[0:40] If we add in another one here, let's say a pear, for instance, then we still end up with apple or banana. If we remove one of these, then we end up with just apple. We're basically extracting the element from there.

[0:53] Inside the problem situation, this doesn't work because we seem to be still hitting never here. Apple or banana is never in this situation, which is really weird. The reason this is happening is when you use a generic like this, when you pull it out into T, then what happens is the members of the union distribute across it. T comes to represent each individual member of the union type.

[1:26] What we can say is, does T extend apple or banana? Sure, that works for apple. That works for banana. Doesn't work for orange. Doesn't work for pear. It maps over them and filters through them. You can think of this as like iterating through the members of that union type.

[1:41] If you don't do this, then this is no longer in a generic context. This is just itself Fruit. It compares the entire thing against the entire thing. It says, "Does this contain apple or banana?" It does contain apple or banana, but it also contains orange. What we end up with is never.

[2:02] This is something that will just catch you out a bunch of different times if you don't know about it, which is why I'm teaching it to you here.

[2:09] There is a really weird solution if you want to do it without a generic context, which is if you say, "Fruit extends infer T" -- this is a conditional type, and you're just inferring the thing that's in the Fruit -- now it will be treated as though now you can iterate over it because it's in a generic context.

[2:29] This T now is basically acting as the iterable. You iterate over. We check against apple, check against banana. If we do, then we return T. A really, really weird property of union types and conditional types that I really wanted to cover.

[2:45] You'll know when you need this. You'll know when this situation comes up because you'll just be thinking, "Why on Earth isn't this working?" You'll realize that, "Oh God. OK, it's distributive conditional types. This one I should put in a generic context first. Then I can mess about with it."