Transcript from the "Inference with Conditional Types" Lesson
[00:00:00]
>> Conditional types have a special tool called the infer keyword, which can be used to extract some sub part of one type from another type. And it's not a coincidence that in the same release of TypeScript where we got conditional types. We also got this infer keyword you might feel, especially once you get used to using infer, you might feel that conditional types are sort of a very weak feature of the language.
[00:00:36] Without this infer keyword, it's almost like this. This is a necessary part of the story and they go hand in hand with conditional types. This infer keyword can only be used within the condition expression of a conditional type it can be used anywhere else in any kind of type expression.
[00:00:58] So, what we're gonna do since it's kind of an abstract concept, we're going to walk through a situation where we find that. The thing infer does for us is conspicuously missing from the language won't go up to a problem that only this new language feature could solve. And that'll help us understand how it ties into the practicalities of what we're trying to build in our In our real world apps.
[00:01:28] So, let's say that we have a class that takes a really complicated options object. And we'll say that it's called WebpackCompiler. And this is actually a subset of the Webpack configuration object. You don't need to remember all this, the whole point of having types is you want autocompletes here, right?
[00:01:48] Cuz it's very, very flexible, like you can do a lot of different things here. But let's imagine that in the source code that we're consuming, it looks just like this, where we do not have an interface or a type alias that defines this. We only have it explicitly specified as the type of the one argument passed to the constructor, and then let's say in our own code, we want something like this where we define a configuration.
[00:02:22] We attempt to compile and then if something goes wrong, we log an error message out. And the configuration is part of that error message. Maybe this code is run on a machine that we don't have direct access to. And it's important that we be able in logs to see that.
[00:02:42] Like if it's a GitHub action, for example, we want to be able to see in the logs like this was a failure here was the config. I can copy this and I could run it. So I simply mean to convince you that This is something you might actually do, right?
[00:02:57] And unfortunately, we have a spelling error here. We've said watch instead of wutch, and we get no type checking help with this spelling error. Why? Well, let's look at the type of this configure object. It's not anything that looks connected to the Webpack config. It's just an object that has two properties on it.
[00:03:21] We're just getting straight up inference here, based on the value or we're setting CFG to and we're passing it In the WebpackCompiler and nothing's objecting there why almost everything on this options object is optional, and we have an entry. That'll work. And the other property will just be disregarded.
[00:03:46] So, we want TypeScript to be able to help us out here. But we can't do this right now. Like if there was something else to import, maybe we could import it, we could use a type annotation here, and then it would start to catch our error. But as I said before, in this case, like all we have is, there's this class that I'm using and has a constructor.
[00:04:10] And the first argument of the constructor wants this thing. So we want to basically pull this type out somehow get a handle on it locally in our app code or our library code, we want to be able to give it a name and refer to it and use it as the type for variables.
[00:04:29] As if we just obtained it as an interface. So this is our goal. We want something like that. Something named that we can refer to, we just gotta get a handle on it first. And the infer keyword is what's going to let us do this. So it lets us abstract some piece of a type from a larger type.
[00:04:55] And before we go back to our Webpack example, which is very complicated, just the nature of that type is complicated. I want to work on an example that has the same approximate structure meaning it still is a one argument constructor. And we still want to obtain this thing, but all of the other noises is out and I think that if we solve this problem, we will also solve the same problem in the WebpackCompiler example.
[00:05:26] So we want to design some sort of type that takes fruit stand or some variant of it as a type parameter. And it emits array of strings. Right like, here's a class. Give me the first argument that the compiler or that the constructor receives. So this is actually the answer here and it looks quite complicated.
[00:05:53] We're going to pick it apart. You will understand every little piece of this syntax, but first I want you to accept that it works. So we've got this, this is sort of the answer here. We've got this fruit stand thing. And we've got like constructor Arg we pass in type of FruitStand.
[00:06:14] And what do we get out? The first argument that the constructor takes. Here's our WebpackCompiler example. We pass in type of that, and we get out. That's the type that we wanted this whole time. We wanted to sort of, grab that first argument, give it a name and we could Give this a name like this with the type alias, we could call it WebpackCompiler options if we want to.
[00:06:39] So this appears to work, it does work. Let's pick apart what all this stuff means here. So first we're just gonna build it up piece by piece. First we have a type alias and we're calling it constructor Arg. It takes a single type parameter, which we're calling C.
[00:07:02] It's gonna be C for class. Next, we want to create a conditional type and, we're gonna be analyzing the static side of a class, which is that's why up here, we're using type of right. That's to get the type of the class, not a type of the instance.
[00:07:27] And we want, To match that against something like this, right, this again is our any constructor type, right? This is this represents anything in JavaScript that is newable regardless of how many arguments it may take, regardless of what it instantiates. If you want to know more specific newable.
[00:07:50] You might put something over here, like any constructor that makes dates. Well, that's gonna be the date constructor. So, we started with this. So all we've added is the condition portion of what will become a conditional type. So this is we've already learned about this right we have our type puram.
[00:08:11] We're testing whether the type that C represents is included in the set that this represents. So is C in the set of all things that are newable. Now, we're about to make a little change that will make this a more specific type. But we want to collect that first constructor argument that this is where the infer keyword enters into the story.
[00:08:36] So we're going to change from accepting any arbitrary list of arguments to accepting a first argument which must be there and we're adding something here it looks like it's a new type parameter kind of, it doesn't it's not going to end up having to be part of our type parameter list.
[00:08:57] But typically we still use title case for this, right? And infer is to the left of it. So you can think of this almost like it's like a vacuum the end of a vacuum, right? We're gonna suck up whatever's here and call it A and after a we're prepared to have zero or more other arguments.
[00:09:22] We don't really care about those. But to maintain the flexibility of this thing that we're building will tolerate other arguments. You don't have to give us exactly a one argument instructor. Are our constructor we just want the first argument of any constructor. Now, the more keen eyed of you may notice, this is actually we've made this type more specific.
[00:09:47] So if we let me get both of these on the screen here, this type matches any newable, including zero argument constructors. This, you're gonna have to have one argument, right? This has to be present. I'm okay with that in this situation because the whole point is we're trying to obtain that.
[00:10:09] So if it sort of Nevers out, right, it sort of grounds out in the case where we use it with zero argument constructors like. I would say that it's a sign of misuse anyway, and that would be a reasonable result. There was nothing to obtain, so never. Right.
[00:10:27] So this is our condition here and now all that's left is for us to fill in what are we emit? What type do we evaluate out to if the condition It's true. And what do we evaluate out to if it's false. So in the case where our condition evaluates to true, we're able to use this type A, just as if it were a type parameter.
[00:10:55] So you can think of this kind of like it's in scope, right? You could use a here or you could use it here. But it's not just a placeholder like a label just so we can give this thing a name. So when people read our code, they can understand what's going on.
[00:11:11] We suck this up with our vacuum, and we can put it out here. Right? So we're saying if C, is a newable. Suck up that first argument that's passed to the constructor and give me its type. Now we need to put something here. And a good way to think about how we handle the other case, right the case where we turn up with nothing.
[00:11:41] We want to make sure that if we use this with a union type, with the or type It kind of disappears. And what I mean by that is whatever type we end up with here, right? Like if we evaluate out to this case, let's say that's x. I want wanna make sure that when we we evaluate an expression like this, we end up with just a string or number it kinda just, it's like multiplying by one, like just an identity thing where it's sort of like vanishes.
[00:12:13] That'll ensure that we're not adding noise whenever this type sort of plays with other types. So we can try that with any and look what happens when we do union any it swallows everything. Union any always swallows everything. The way I think about this, it's like if you send your friend to go to the grocery store.
[00:12:43] You're saying pick up three organic eggs, and 25 grams of some spice. And also get me everything in the store please. Well, you could have just led with that right? You could have just said, give me everything in the store. That's why this swallows everything. It's kind of like multiplying by zero in some way, in that if this is found in a union type, if you ever see this, it just makes the whole rest of the union type pointless because it's just going to evaluate out to any.
[00:13:21] So not only is this the wrong answer, this is not what we desire. It's like the exact opposite of what we desire. We want something that disappears, not something that dominates over and kind of takes over. The whole type. So if this gives us the exact opposite of what we're looking for, it makes sense to me to try the opposite of any, right?
[00:13:43] So we're going to try this with never, and we in fact do get what we want. So when you're making these conditional types, in the case where something didn't match and you kind of just want to like escape out let's just like forget about it. Often you're just gonna want to admit and never type.
[00:14:01] Because that well, when used in a union type like this it will just disappear. No noise, so let's apply that to our constructor, our extractor, and we can see this is the answer. This is what we ended up with. So just again, to recap, it's a generic type that takes a type parameter C, we look at sea and we see If it's a newable, and if that newable takes at least one argument, we have a match.
[00:14:42] And in the event that we have a match, we're going to use our infer keyword which is like a vacuum. We're gonna suck up whatever's in that first argument, and we're going to put it in a box called A. And then, in the event that we had a match, we will always enter this branch.
[00:15:03] So you're always going to enter this branch, which is why you're gonna wanna use eight here. Often down here, you could put another ternary operator, you could chain these together. And you could say, well if it's a one argument constructor, we do this. Maybe if it's a two argument constructor, we build a tuple with the two things.
[00:15:23] You can sort of chain these together. It's normal by the way to see this chaining because, unlike in the value based JavaScript world, there is no more verbose and explicit equivalent to the ternary operator. We can't like expand this out into if then else. I know that some people don't like nested ternary is for that reason.
[00:15:44] So finally let's look at at the result here. So no matter what we use this type with, we can see that we get the result we're looking for. We get the first argument the constructor takes. So you can create a new date like literally new date. You can pass it a string, a date as a string, a number, which is like milliseconds since the epoch.
[00:16:12] And then you could create a date from another date here is the resolve and reject. you're used to seeing in a promise constructor. Here's our Webpack config. So now we have this ability to using infer, to suck up a part of a type and to obtain it and to create maybe a local reference to it in some way, taking this all the way to the finish line.
[00:16:39] If we go back to our first example, our motivating use case where we had the spelling error, and instead of just creating this object with inference taking care of the typing, we add this here we give it an explicit type annotation. As we've done right up here, we can see that not only do we get an error where we hoped we would get an error.
[00:17:06] TypeScript is found that there is a watch property and the word we used is pretty close to watch. So maybe it's a spelling error. Even guessing that it might be just a spelling mistake. So there you go. This is a great use of the infer keyword. And I use it all the time for this exact use case.
[00:17:33] Where I'm consuming some code and I wish I could obtain this type maybe because just for logging reasons or something, I want to like create the things that I'm passing into the function. First, log that out and then pass it in. And this is a great way even if those entities those types are not available for direct import by whoever made the library.
[00:17:58] That doesn't hold you back, you can still do this. So, be caution that I want to give you is in a very large app, be careful that you don't do this too too much, because it's not simple for the type checker to evaluate. And if you end up with with too much of this kind of behavior here too many conditional types, too much infer you can end up slowing down your the speed of your type of heads.
[00:18:34] Now that's not I would say only worried about this if you start experiencing that problem. It's not the kind of thing where, you want to make sure you avoid the problem so that you don't get out of control in the first place. Really, if you're looking to improve your performance, just make sure If you're not making like overusing this this feature of the language.
[00:18:57] But totally fair to use totally, the only tool for the job in many cases.