TypeScript Fundamentals, v3 Nullish Values
Transcript from the "Nullish Values" Lesson
>> The next topic we're going to discuss is nullish values and the three specific kinds of values that I'm talking about are null, undefined and void. So we're gonna talk about each of those three when I advise you use each of them. And then we're going to discuss the non-null assertion operator where you cast away any possibility of something being undefined or null.
[00:00:26] And then the definite assignment operator which can be used in some interesting edge cases around class initialization, or instance initialization, so null. Null indicates that there is a value for something and that value is nothing. I wanna be clear that different people have different opinions about this, when to use null, when to use undefined.
[00:00:55] But this has served me well, so for example, if I receive a null return value from a function or I find null on a class field, I take that to mean it's almost a 404, right? Like a not found, nothing's there or nothing's there yet, well really just nothing's there, like something's arrived and that is nothing.
[00:01:22] Undefined, I take this to mean that either we haven't gotten to providing something or we're not ever going to provide you with something. So here's an example of a form, maybe we're like monitoring how long it takes for a user to complete this form. And completedAt is a property that is undefined because they haven't completed it yet.
[00:01:50] And when they complete it, we will get a value so I prefer to use undefined for cases like this. Void as we said before, this should be used explicitly for function returns and it means that the return value of the function should be ignored. If you wanna learn or refresh yourself more about how to use void, you can look at the functions chapter where we show the impact of using void versus an undefined return type on callback types.
[00:02:25] So that's null, undefined and void, so now let's talk about these two operators. First, there's a non-null assertion operator and you may think of this as kind of similar to optional chaining, if you've ever used that, this sort of dot, I think it's click. Yeah, it's like dot question mark, something like that, I don't use it very much, to be honest.
[00:02:51] But that sort of allows you to reach deeply into an object and if at any point in accessing a chain of property something is found to be undefined, it will sort of just give up, just evaluate out to undefined and just sort of skip it. Non-null assertion operator is sort of the last or the less forgiving variant of this.
[00:03:15] Here's an example, so let's say we have a grocery cart which can contain fruits and vegetables. We initialize it without either and we try to kind of reach into this and say like, okay, I hope fruits is there because I want to treat it like an array and push stuff into it, but this array might not be there.
[00:03:38] In fact, it's not there, because we initialize this to an empty object. We're correctly being busted, on trying to use something that might not be there. Well, if we use this non-null assertion operator, this exclamation mark right here, we're telling TypeScript to disregard. Got Glenn Marth, I think I actually might have a typo, I do have a typo, this is the typo.
[00:04:08] That's a good catch, so it should not be dot bang, it should be bang dot. [LAUGH] Yep, very good catch, thankfully, my code example works because it's actually running TypeScript behind the scenes and it compiled. So this is working in front of our eyes, but I'll make that correction, thank you.
[00:04:28] So we can see that the possibility of fruits being undefined has sort of been thrown out, we're just saying please disregard TypeScript. I know what I'm doing, I'm the software engineer here, just do what I tell you to do. And as a result, we can kind of proceed through, I use this a lot, but never in app or library code.
[00:04:52] I use it like crazy in test suites because I prefer test frameworks and assertion libraries to be clear that will regard a throw as a test failure. So if this is my test case, where I have nothing, I don't have a fruits array in my grocery cart, and this throws.
[00:05:15] That's a failed test and that I'll treat it as a failed test, it's almost like a little mini assertion I think this is beautiful in a test suite. I don't like it in app code because it's a pretty hard failure to hit and I would much rather use a type guard and set and check to see if this is undefined.
[00:05:35] And then let's reach into it, I think that is a more fault tolerant way of handling this kind of situation. Because this in almost every case like this is this could have a real possibility of being undefined. I don't even like the idea. If we were to do this, I don't even think that this is justified because you're like I slightly inattentive refactor away from moving this somewhere else where you might not have that guarantee.
[00:06:15] I prefer to have my guarantees by way of type cards that will actually do some runtime thing to evaluate whether something's there or not there. Compared to this, which is just saying force the type system to forget about the possibility of undefined. That's a dangerous copy paste away from creating problems for you.
[00:06:38] But great in test suites because throwing errors in test suites, when things turn out in an unexpected way. That's a test failure, that's great, that will tell you something's wrong, you can go and fix it. All right, definite assignment operator, it's the same exclamation mark syntax used in a different place and that place is on class fields.
[00:07:06] So here's an example of something, essentially a slightly relabeled piece of code that I have in production right now. And it's a class that has to do something asynchronous as part of its setup procedure. So constructors can't be async but they can kick off an async method as they finish.
[00:07:31] And so what I do here is when the constructor is invoked, I say I've got this setup promise and I kick that off. And I say, we begin in a non setup state and I'm going to, Chain in the promise returned by this method here, right? So that means that this setup promise will only resolve once this thing is done.
[00:08:00] And then when everything's done, I'll flip that, his set up flag and say, okay, we're now ready to go. So I'm getting an error here and I'm seeing that this property has no initializer which is true. And it is not definitely assigned in the constructor, what does definitely assigned mean?
[00:08:22] It means that TypeScript wanted to see, it wanted to see something like this, which I guess I could have done. It has no idea if or when this is going to be invoked. And so it's not willing to, doesn't wanna give me a guarantee that this property, this class field will get its value.
[00:08:47] Now if something's directly in a constructor, TypeScript can understand that. It can analyze it and it can say, if we instantiate this thing and then we say, here's a thing with async setup. Well, by the time I give it to you, it will have a Boolean value there because I saw you set one on the constructor but down here, it's not so sure.
[00:09:07] Now, in this situation, I know something that TypeScript doesn't and that is that the callback you pass into a promise constructor, it is invoked synchronously. Which means that by the time we actually get that new promise, this thing that I've selected, that little callback, it's already been invoked and it's already finished.
[00:09:31] And I will definitely have my Boolean value there but TypeScript, It doesn't have a way of saying, well, this is a synchronous callback. And this is an async callback and I know about the runtime behavior here. It just has no way of describing that but in this case, this code, it's actually okay.
[00:09:50] So, We could add something here, I'll just have to show you in the example. All we'd have to do is add the exclamation mark. This right here, the definite assignment operator, it's saying, look, I will assume responsibility for making sure this thing gets its value. A great place to use this is component lifecycle hooks.
[00:10:22] So no matter what component library or framework you use, there's often like there's the constructor logic. And then there's some initialization process that happens later, maybe it's when the component's initially rendered or when it's about to render and maybe you take care of setting some stuff up there.
[00:10:44] So realistically, you're not doing much with this thing, Until it's set up, and maybe it's rendered. So you might say, I know yes, after your constructor runs, this thing is not gonna be there. But realistically, for all things I care about, we can act as if it'll be there, so that's the kind of time where you might wanna use this.