Type Transformations Workshop (9 exercises)
solution

Express Logic Inside a Conditional with Infer

This challenge has two solutions.

Solution 1

The first solution might look pretty similar to pieces that you've kind of understood before:

First we use extends to see if T has an object with a data attribute.

If it does, then we can just return it by using an index access type. Otherwise, we return never.

Solution 2

While that solution is fine, in the exercise intro we mentioned that this challenge can be solved by using infer.

Here's what the infer solution looks like:

Let's break it down.

The infer in T extends { data: infer TData} says "Whatever is passed in, infer its type".

Then the infer declares TData for the positive condition. In order words, the TData variable is only defined for one branch.

GetDataValue in Action

To check our work, we can create a new Example type that passes {data: 1} to GetDataValue:

This would extract the 1.

If we change GetDataValue to return 1 or undefined:

Our Example type would show 1 | undefined when hovered over.

Which Solution to Choose?

So, when you're choosing these two possibilities, which one should you pick?

I tend to prefer the second solution because infer gives us an opportunity to name a new variable.

To make our code more clear, we could rename TData to TInferredData:

Compare this to the first solution, where we had data typed as any then extracted it with T["data"]:

I prefer using infer because the logic is expressed inside of the conditional check.

The next time you find yourself inside of a conditional check and you need to extract out something, I suggest you harness the power of infer!

Transcript

[0:00] This problem has multiple solutions. The first solution might look pretty similar to pieces that you've understood before. We're first checking if T extends data any here. If it does, then we know that T has a data attribute on it, and so we can just return that data attribute. Otherwise, we return never.
[0:20] This, we're using an index access type, which we've already seen. This is pretty cool. The one I wanted to point your attention to though is this infer here. What's going on here instead, now what we're saying is T extends data infer TData. Instead of this any, we've now got this infer attribute in the middle, which is saying whatever is passed in here, then just infer its type.

[0:50] What this does is it declares a TData just for the positive condition. This means we can't access TData here because it says cannot find name TData. That variable isn't defined in that branch. It's only defined in this branch here. This is a way of adding another variable into the function.

[1:12] We can say, if we had TData up the top here, then this is kind of the same thing, except it would be declared in the whole scope. What infer does is it allows you, inside the scope of a conditional check, to pattern-match against the thing you're checking and extract out a new type variable.

[1:31] What this means is that TData now, we've extracted it, and it could be anything that's inside this TData or inside this data slot. We just then return it. If we say type Example = GetDataValue -- let's say we have a data, and let's say it's just a 1 here -- this is going to extract the 1. We could even do TData or undefined here, and we end up with 1 or undefined.

[1:57] When you're choosing these two possibilities, which one should you pick? I tend to prefer the second solution because this infer TData gives me a new opportunity to name a variable. We could even call this TInferredData or something here. For me, this reads pretty nicely in terms of the syntax here.

[2:21] With this one, we're saying data.

: [2:23] any and then extracting it there. Whereas here, all of the logic is expressed inside the conditional check. This infer, it's really, really powerful. I suggest that the next time you're inside a conditional check, and you need to extract out something interesting, you use infer because it is extremely powerful.