Unions and Narrowing 28 exercises
solution

Narrowing Behavior Across Scopes

Remember, scopes are a JavaScript concept, not a TypeScript concept.

Let's start by looking at all of the different scopes there are in the code example:

const findUsersByName = (
  searchParams: { name?: string },
  users: {
    id: string;
    name: string;
  }[],
) => {
  if (searchPara
Loading solution

Transcript

00:00 Let's start this explanation by examining all of the different scopes there are in this file. And scopes are a JavaScript concept, not a TypeScript concept. So we have the module scope, right? If I were to declare something inside here, const example equals 123, then example is declared within the module scope, the scope of the module.

00:18 If this wasn't a module, if it was a script instead, then this would be in the global scope. But because we've got imports and exports, it's a module. So we've got something in the module scope. So findUserByName, that's a function declared in the module scope, and it has its own function body, which is its own scope, right?

00:35 We can declare a variable inside here, const example equals 123. And then we can't access example outside here, right? They're not in the same scope. So, great. We've then got another scope inside here, inside this if statement. Wonderful. And then another scope inside the function body.

00:53 So actually, I can make this clearer by adding some curly braces and in return like this. Now, scopes have an impact on how TypeScript handles narrowing in certain cases.

01:03 So what we have here is inside the if statement, you notice that before when we've tackled narrowing, we understand that searchParams.name here, we can call toUpperCase on it. And TypeScript will consider this safe because we've checked if searchParams.name exists, right? It's not undefined.

01:21 And so we're safe to call Uppercase on it because it's never going to be undefined. But if we move it into the upper scope where we haven't done that check, no way, right? It could be undefined. Absolutely. But then if we move it even to a function scope inside here, it becomes all undefined again. What? How is that possible?

01:41 How is that possible? Well, what's happening here is that .filter, right? We're passing a function to it. Now, we happen to know as JavaScript developers that filter is called synchronously, right? It's just called filter, filter, filter, filter, filter like that.

01:57 It doesn't -- there's no gap in between where searchParams.name might be mutated or changed or things like this. So .filter, right, it's synchronous call. But TypeScript doesn't know that and there's no way to annotate that.

02:11 Filter could be a function that's like a subscribe function like addEventListener that's called at a later date when searchParams.name might have been modified by something else.

02:21 So TypeScript can't know that inside this if statement, right, that everything below that is going to be a -- is going to be not undefined. So how do we solve this? Well, one way to do it is we can save it to a variable.

02:37 We can say const name equals searchParams.name. Now, and if we take name and we put it inside here, now it starts working. Look at this. So const name equals searchParams.name. The reason this works is fairly easy to understand.

02:54 Name is never going to be mutated because it's saved in a const. So this means that filter function is pretty safe to assume that name is always going to be the same thing and name is considered a string. This means in this scope, so the if statement scope and in the function scope below it, it's always going to be a string. What if we change it to a let?

03:15 Well, let happens to work too because TypeScript actually tracks whether you've assigned something to a let. This actually happens since TypeScript 5.5, which is why I'm wearing a different outfit from the -- from the problem setup.

03:29 So now name, like this, if we were to reassign it, we can say name equals undefined. Yeah, now TypeScript has tracked that we're assigning it to be undefined and it actually tracks that it could be undefined in this spot. Same as if we put it inside here. What if I do it as a string? Yeah, it tracks that too.

03:47 So TypeScript has actually gotten a bit smarter here since I first recorded this exercise. So the way you get around this narrowing in different scopes is you save it into a variable and that might feel a little bit awkward. Why can't TypeScript actually track like object properties in the same way that it does variables?

04:04 Well, object properties can do slightly crazier things. They -- you can have getters inside them that you can't have in variables. And so TypeScript tends to trust these properties a little bit less than it does just with pure variables.

04:19 So if you're having this kind of issue where you have narrowing in different scopes and everything's going wrong, then save it into a variable, either a const or a let, and the errors will magically disappear.