Transcript from the "Type Guards" Lesson
>> So we've got two big topics left in this course. The first is type guards and narrowing, and then we're going to touch on generics as our final topic. Type guards are really important pieces of code. You can think of them as a junction between something with special meaning to the type system and something equivalent that, actually, it's evaluated at runtime.
[00:00:31] A great example of this, or several examples, is this here, which we saw before. Where we're like, okay, I've got this function that could return some tuples. I'm gonna use the first element of the tupple, To narrow, right? To handle the error case, and then here's the else down here.
[00:01:15] This check at runtime will only pass if the first element of the tuple is the string "error". And we know that it's safe to narrow down this way if that check passes. There are lots of other built-in type guards that you can use, so here are a variety of them.
[00:01:32] We've seen this one, instanceof, we've seen this one today, typeof, there's a specific value check. We saw this with discriminated unions, remember? When we were checking explicitly if the first element of the tuple is the string "success" or "error", we were able to use that. A truthy/falsy check is a type guard, right?
[00:01:52] If you have a value that could be undefined, it can become defined. There's a built-in array function called isArray, property presence check, right? This is just seeing if value has date range in it. And I think that's the full list. Now let's go through, let's look at this.
[00:02:13] We've got this value, which could be a date, null, undefined, the string "pineapple", a tuple of size 1 containing a number. Or this object containing a date range with a pair of dates. So this is one big conditional here. And we can see how we use these type guards to narrow down into specific little cases.
[00:02:34] Where we know we can handle this value in a specific way, depending on the type guard that's applied. Here, instanceof Date, we narrowed down to Date. Here we're checking to see if the value is a string, and we know "pineapple" is the only possibility that this thing could be.
[00:02:51] That's the only option here that's a string. Here's the specific value check. We're checking to see if it equals the value null. Null is the only thing it could be, so we kind of were crossing these off the list, effectively. Now we can apply a truthy/falsy check, and it could have been null, but we handled that up here already.
[00:03:12] And we're in the else block, so we know that it's not null, it has to be undefined. If it's still falsy, it must be undefined. We can check to see if it's an array, and then we get that tuple. We can check to see if it's an object, I mean, actually, by this point, this is the only thing it could be, right?
[00:03:35] So we're checking to see if dateRange is in the object, and it is. And down at the end here, we get this thing called never, remember that's the type, call it a bottom type, it represents the empty set. What this means is we're all finished, there's nothing else it could be.
[00:03:50] We've handled every possible thing it could be using these type guards. And within each of these code branches, we have a very specific type that we can act on. We can define our own type guards, so let me go back to our code here real quick. There are the built-in type guards, here are our user-defined type guards.
[00:04:18] So let's say that we have this interface, CarLike, something car-ish. And we have this value which is an any, could be anything, right? We wanna determine whether it's a car, and we wanna do that just exclusively based on shape. So, what this would do, it's very verbose here, we're gonna work our way towards things that are a lot more sane than this.
[00:04:45] We first have to see, okay, is this is this truthy or not, is it null, is it undefined? All right, we check to see if it's an object. We check to see if make is a property in the object, and the value of make is a string. And we do the same thing for the other fields.
[00:05:02] And then when we get down here, we get nothing, doesn't seem to be working for us. But this is what you would have that represents this check. Part of the challenge here is, there's so much going on here. And we start with an any, it's just not able to narrow it down.
[00:05:28] So what we can do instead, we need this user-defined type-guard. We can define this function that says isCarLike, and we put that same check in here. And what we would hope is, when we're done, we can write something like this, where we'd say, if isCarLike, and then we'd get the type car in here.
[00:06:21] ValueToTest, isCarLike. Now, looking at this, what is the actual return type here? Forget what I just wrote, what's the type of the thing I'm returning?
>> It's a Boolean, you're absolutely right, it is a Boolean. Type guards should return Booleans, they need to. This tells the compiler what the Boolean means.
[00:06:53] If the Boolean is true, it means valueToTest is a CarLike. If the Boolean returns false, it means it's not a CarLike. And that's what lets us have a user-defined type guard here. It's not just about returning a Boolean, it's about this line of code here, this expression, that tells the compiler what the Boolean means.
[00:07:18] It's what connects this stuff that runs at runtime with what it means and what we should take it to mean at build time. And it's very important that you keep these things well aligned when you write user-defined type guards. If you write these poorly, if you write type guards that lie to you, the lies will be very confident.
[00:07:41] And you'll have weird stuff popping up throughout your application. So it's very, very important that you kind of keep this well-aligned with this. And as long as you do that, these things are great. They help you handle union types beautifully, it's common to use these for API responses that you've received.
[00:08:04] And you don't know what this JSON thing is, but you pass it to one of these. And then you get a great type that you can send downstream to the rest of your code and it's a very nice type of response. But it has to be consistent, the runtime and the meaning of the check have to be in close alignment.
[00:08:24] So that is a type guard of the value is foo form. There's another thing we can write here, slightly different. Oops, yeah, that's great, and I'm gonna get rid of the bottom thing. Okay, this is a different flavor of type guard, and I want you to look at this return type here.
[00:08:59] It still returns a Boolean, we've inverted it, there's the bang and parentheses around it, so we flipped it. And we're saying, if it turns out to not be CarLike, throw an error. Now we can say assertsIsCarLike, and we say this, it asserts that value to test is CarLike.
[00:09:22] And instead of having to use it in a condition like this, we can use it like this. Effectively, it's the same concept, except we're saying, look, it's not about if-else, it's about throw versus not throw. The fact that we made it to the next line, the fact that we're on line 114 and haven't thrown, that means it's a CarLike, whereas up here it was an any.
[00:09:54] These are also good, it just depends on what you're doing, whether you wanna throw or not, right? I would use these when you intend to throw, when you should throw. All right, we're gonna come back full circle to classes right now. So remember our private field presence check, right?
[00:10:17] We use that to make our equals method. Well, I mean, remember how nice this was? The fact that we can see this field here means it's definitely a car, right up until this point it was an any. But getting past this one check told us that it's a car.
[00:10:35] I love static methods as user-defined type guards here, because it can see private data, and this is a pretty bulletproof thing. I mean, just consider how good that looks versus this. Now, they're not equivalent, because this kind of type guard is much more conducive to a structural type system, where you're really just dealing with issues relating to shape.
[00:11:03] Not issues relating to which constructor you came out of. Sometimes this is what you want. Sometimes you want nominal style type guards, and this this will work beautifully. I would just recognize which each one is, they're both tools in the toolbox, and this one is really nice. So that I've just taken our .equals method, and removed the comparison of serial numbers, and I just returned true, if we make it this far.
[00:11:30] That's all I did here. But the presence of a private field, it is conclusive, the fact that you can see it at all means you know what this thing is. You could do this, and then, instead of returning false down here, maybe you kick out and you check some structural things.
[00:11:52] You got options, but they're both kind of good tools. And we can see proof that it works, although it will, there's car, there's any. But static methods are pretty good with that. This is something that was added pretty recently, I think it's a 5.3 feature, the ability to narrow with switch (true).
[00:12:21] So I've created two classes here, a fish and bird, fish can swim, birds can fly. And I can write a nice little switch statement like this, which some of you that come from other programming languages, this is a very common construct to see. If you've ever heard the term pattern matching, this looks a lot like pattern matching, right?
[00:12:41] If you've used Scala or Erlang or Elixir, there are lots of languages that have pattern matching like this. So effectively, we're saying, I'm going to switch on true. But really it just comes to each case clause being compared with true. So we're gonna keep going down these case clauses until we find one that evaluates to true.
[00:13:07] And so here, did this work? val instanceof Bird, Wait a sec. I suspect I know what's going on here. Yep, it's definitely a 5.3 feature, because it's not working in 5.2.2. Here's 5.3, hey, there we go, super, super modern. VS Code likes to troll me by switching which TypeScript version it uses without telling me.
[00:13:45] But this looks very neat, especially if you have a long chain of things that you're trying to process. It's nice to have an option to write things this way. So my advice for writing high quality type guards, I talked about this a little already, but here's an especially bad one here.
[00:14:07] Write a type guard that's supposed to check if val is specifically the value null. And we're writing it as if null is the only falsy thing that could exist. The number 0 will pass through here, an empty string will pass through here, the Boolean false will pass through here.
[00:14:21] Undefined, it's just, these are all really bad, these are bad. We would fix it by saying val, Something like that. This would be a silly type guard to write because you have a built in one, this right here. But this is a particularly harmful place to lie in your types.
[00:14:42] Cuz TypeScript will take you at your work, and it will trickle off throughout your code. You'll think that I sold you a bill of goods, that I told you that this will help catch bugs, and instead it's creating bugs, so, be careful in this area.