TypeScript Fundamentals, v3 Type Guards & Narrowing
Transcript from the "Type Guards & Narrowing" Lesson
>> So, we've talked about top types, bottom types. Let's talk a little bit about type guards, and narrowing. So we have already addressed a bunch of different kinds of built in type guards, right? So I've created a value here that could be it's not quite an any but it could be a bunch of different things.
[00:00:20] And we can see a bunch of different ways that we can narrow it down, get a branch of code that will execute only in a specific scenario. And then within that code block, we can treat it in a more useful way, right? Like, as it stands, there's not much you can do with value given that it's so varied like what it might be.
[00:00:42] So here we could say, okay, it might be a date, so we use instant serve as a type guard. Type of, right? That works just fine. Checking whether something's undefined or not, the type of is great for that. Checking against a specific value that's a perfectly fine type card to use.
[00:01:00] Truthy, falsy checks are fine. Just beware, of course, empty strings and zero and false are all false II values. So if you have something like it's Boolean or undefined, truthy, false, you might catch something you're not intending to catch, right? Some built in functions have type card capabilities like array.is array, right?
[00:01:25] So we can see that value comes in could be a bunch of things. But within this block, it can only be a couple of length one that contains a number. Property presence check with this in keyword, right? That's saying is does there exist an owned property called date range, within value.
[00:01:54] And then of course, you can always like make a couple checks and then whatever's left over, that'll be what's left over. So that's sort of like a reverse type guard, right? Can carve away the parts that you don't want. And all that's left is what's left. So those are built in type guards.
[00:02:12] And I think this is an exhaustive list. But let's talk about user defined type guards because often, we want to operate on a much higher level than simply saying, is this a string? Is this a number? We more want to think about if you're building the front end masters website, you'd want to say is this a learning path or is this a course?
[00:02:35] Or is this an instructor or is this a user account? And that's a lot more complicated. It's harder to think about how you might squeeze these kinds of things together to achieve that same level of creating a branch and knowing that it's this more complex object as you go down that path.
[00:02:54] So that's where user defined type guards come in handy. And here's an example of how ugly things could potentially get, right? So if we wanted to see, is maybe car doesn't look like a car. Well, in order to do this safely, given that it's an unknown, right, remember, we can't use this unknown until we narrow it down with a type card and check it out and make sure it's what we think it is.
[00:03:22] So you'd have to see okay, does it? Is it knowledge like Could it be null or undefined? Well, we eliminate that with this. And then Okay, does it have like is it an object of an object type? Does it have make model and year our make model and year strings numbers.
[00:03:40] Do you could do this. Let's say you were willing to do this, right? I would argue, you're already out of your mind if you want to write a bunch of code that looks like this. But let's pretend you're out of your mind. So, even if this did work, Eventually, you're gonna wanna refactor this.
[00:03:59] You're just gonna wanna take that same code that's sort of a bunch of different stuff in your conditional. You move it out into a function just so that your code can look like this, right? But the problem here is, even though these are all type guards, there's nothing about his car like there's nothing about this function that says, hey, TypeScript, you should regard the true or false value that this function returns.
[00:04:29] As an indication of what the type the argument is, right? Like, right now, even if we added type annotations here, right? Even if we said this returns a Boolean instead of an any there's nothing that tells TypeScript that something special about that Boolean that it has any bearing on what should be assumed about the argument we passed in.
[00:04:56] And so even the imperfect narrowing effect that we had when we had all this stuff up here, like at least we saw that it was an object, right? I mean, really, that just came from this here. But we don't even get that here. It's back to unknown. We're like back to square one.
[00:05:14] So user defined type guards are the solution to this problem. And the problem we're trying to solve is TypeScript has no idea that the return value of his car like has anything to do with what we passed into it. So here we go. We're going to make this a user defined type guard.
[00:05:36] And we're going to start by talking about an is type card. Value is foo, right? So we're going to see that syntax somewhere in a moment. So here we go. It's a function is car-like. It takes an argument. Typically, you're going to want this argument to be something very flexible.
[00:05:56] Why? Because you're going to reach into it. And you're going to check for the presence of things. You're probing, right? You're inspecting it, you can't assume that you know what it is yet, but you need it to be flexible enough to perform the validation, that makes sense. And then what makes this a type guard is the special way of writing its return type.
[00:06:19] So we can say effectively that like Yes, his car like it returns a Boolean. But that Boolean should be taken as an indication of whether value to test conforms to the type car like. And we can write our same implementation here. But look at this. Out here, it's unknown.
[00:06:49] And within this block, it conforms to the interface. And it's only because you're telling TypeScript to trust you. So, this is very important. This type guard can be your best friend or it can be your worst enemy. Because let's just completely do something crazy. What if we did this.
[00:07:19] Just anything you give me, I'm just gonna say it's always car like. Well, there it is, I'm not narrowing the value anymore if that's what's happening. So there's our -19. It'll be change this to x. Like apparently -19 is a car like, like I can access make model and year on this thing.
[00:07:49] No errors, None, no little red dots in my scroll bar here. So the type car is only as good as your alignment between the actual checking logic that you implement and the claim you make, about what that logic means means in terms of the type, of what's passed in.
[00:08:16] If your logic here is flawed, you're gonna run into big problems, because type scripts got to do exactly what you say. Right now I'm saying everything's carlike. And so -19 apparently has a property called year on it, TypeScript doing what I'm telling it to do. So just be really careful about these.
[00:08:40] These are again, I said this before I want to say one more time. These type guards are the glue between compile time validation and runtime behavior, you got to make sure that your compile time validation and your runtime behavior match up. And if they don't, you're gonna be lying to yourself here, right?
[00:09:01] You're gonna be allowing yourself to comfortably reach in and pull things off and operate on values that should be erroring. Let's look at the other type of user defined type card. And it follows this format, asserts value is foo. So, here's what the return type looks like. I've changed this very slightly, right?
[00:09:25] But that the return type is changed a little bit. The type guard itself if the check is the same check we were performing before, if it is found to not be car like we throw. So if you pass through this function and we haven't thrown your car like that's what we're saying here.
[00:09:52] If this function finishes without throwing, that means value to test his car like so effectively this function asserts that the value to test is car like. As a result, we can do something really cool here. We don't have to use a conditional because we're sort of using the return versus throw branching, well, I don't necessarily agree that that's control flow.
[00:10:20] It is definitely a true statement that will only reach this line down here if we pass through the function without throwing, right? But this is great for modeling, things that are in test suites or things that really are exceptional validation where if, like you don't even know how to proceed unless this is car like.
[00:10:45] So we want to get something more specific like we get we can't operate on this waltz and unknown and we legitimately do want an error to be thrown if this is some weird value. So in a might be the right thing to do here. But this is the other kind of type card that you can use.
[00:11:02] I commonly will use both where I would create something that's like is car-like and then I would consume this and kind of wrap it in something that'll throw it in error. So I get both of the funding is typed guard. And then I'll use that same little Boolean returning function in my asserts type guard.
[00:11:29] So here's a terrible example of type guard. And you can see like, well, I already showed you an even more terrible one. But here I'm assuming that all false things are no. Well, this will make you think zero is now an empty string is now like the false Boolean value is no, obviously, there are false things that are not no.
[00:11:50] But again, TypeScript will. It will follow your instructions, right? So in this case we have empty and we have zero. And we're here saying, if this value is no then log and unfortunately, we're just catching way, way, way too much with this type card. And we're never going to be able to get here, right?
[00:12:14] It's claiming that It's impossible that we'll get here.