Fundamental Type Guards
Tags: typescript, programming, tutorial, beginners
Published: 2022-06-09 11:45:51

TypeScript Narrowing #2

{% embed https://www.youtube.com/embed/ZdMfiVK7fcM %}
_See this and many other articles at [lucaspaganini.com](lucaspaganini.com)_

Hey!

Welcome to the second article in our TypeScript narrowing series, where we go from fundamentals to advanced use cases.

## Our Goal

In this article, I want to show you the fundamental type guards in practice. To do that, we will build a function that formats error messages before showing them to the end-user.

```ts
const formatErrorMessage =
  (value: ???): string => { ... }
```

Our function should be able to receive multiple different types and return a formatted error message.

```ts
const formatErrorMessage =
  (value: null | undefined | string | Error | Warning): string => { ... }
```

Are you ready? 🥁

## The `typeof` Operator Guard

We'll start by supporting only two types: `string` and `Error`.

```ts
const formatErrorMessage =
  (value: string | Error): string => { ... }
```

To implement that function, we need to **narrow** the `string | Error` type to just a `string` and deal with it, then narrow it to just an `Error` and deal with it.

```ts
const formatErrorMessage = (value: string | Error): string => {
  const prefix = 'Error: ';

  // If it's a string, return the string with the prefix

  // If it's an Error, return the Error.message with the prefix
};
```

The first type guard that we'll explore is the `typeof` operator. This operator allows us to check if a given value is a:

- `"string"`
- `"number"`
- `"bigint"`
- `"boolean"`
- `"symbol"`
- `"undefined"`
- `"object"`
- `"function"`

Let's use it in our function.

```ts
const formatErrorMessage = (value: string | Error): string => {
  const prefix = 'Error: ';

  // If it's a string, return the string with the prefix
  if (typeof value === 'string') {
    return prefix + value; // <- value: string
  }

  // If it's an Error, return the Error.message with the prefix
  return prefix + value.message; // <- value: Error
};
```

What's happening here is that the `typeof value === 'string'` statement is acting as a type guard for `string`. TypeScript knows that the only way for the code inside that if statement to run is if `value` is a `string` so it narrows the type down to `string` inside the if block.

Since we're `returning` something, `value` can't be a `string` after that if statement, so the only type left is `Error`.

## The `in` Operator Guard

Sometimes we're not so lucky. For example, let's add a new type to our function, a custom interface called `Warning`.

```ts
const formatErrorMessage = (value: string | Error): string => {
  const prefix = 'Error: ';

  // If it's a string, return the string with the prefix
  if (typeof value === 'string') {
    return prefix + value; // <- value: string
  }

  // If it's an Error, return the Error.message with the prefix
  return prefix + value.message; // <- value: Error
};

interface Warning {
  text: string;
}
```

Now our code is broken.

```ts
const formatErrorMessage = (value: string | Error | Warning): string => {
  const prefix = 'Error: ';

  // If it's a string, return the string with the prefix
  if (typeof value === 'string') {
    return prefix + value; // <- value: string
  }

  // If it's an Error, return the Error.message with the prefix
  return prefix + value.message; // <- value: Error | Warning
};

interface Warning {
  text: string;
}
```

Before, our `value` variable could only be an `Error` instance after the if statement.

```ts
const formatErrorMessage = (value: string | Error): string => {
  const prefix = 'Error: ';

  // If it's a string, return the string with the prefix
  if (typeof value === 'string') {
    return prefix + value; // <- value: string
  }

  // If it's an Error, return the Error.message with the prefix
  return prefix + value.message; // <- value: Error
};

interface Warning {
  text: string;
}
```

But now, it can be `Error | Warning` and the `.message` property doesn't exist in a `Warning`.

```ts
const formatErrorMessage = (value: string | Error | Warning): string => {
  const prefix = 'Error: ';

  // If it's a string, return the string with the prefix
  if (typeof value === 'string') {
    return prefix + value; // <- value: string
  }

  // If it's an Error, return the Error.message with the prefix
  return prefix + value.message; // <- value: Error | Warning
};

interface Warning {
  text: string;
}
```

The `typeof` operator won't help us here because `typeof value` would be `"object"` for both cases.

```ts
const formatErrorMessage =
  (value: string | Error | Warning): string => {
    const prefix = 'Error: ';

    // If it's a string, return the string with the prefix
    if (typeof value === 'string') {
      return prefix + value // <- value: string
    }

    // If it's a Warning, return the Warning.text with the prefix
    if (???) {
      return prefix + value.text
    }

    // If it's an Error, return the Error.message with the prefix
    return prefix + value.message // <- value: Error | Warning
}

interface Warning {
  text: string
}
```

One of the idiomatic ways of handling that situation in JavaScript would be to check if `value` has the `.text` property. If it does, it's a `Warning`. We can do that with the `in` operator guard.

```ts
const formatErrorMessage = (value: string | Error | Warning): string => {
  const prefix = 'Error: ';

  // If it's a string, return the string with the prefix
  if (typeof value === 'string') {
    return prefix + value; // <- value: string
  }

  // If it's a Warning, return the Warning.text with the prefix
  if ('text' in value) {
    return prefix + value.text; // <- value: Warning
  }

  // If it's an Error, return the Error.message with the prefix
  return prefix + value.message; // <- value: Error
};

interface Warning {
  text: string;
}
```

This operator returns `true` if the given object has the given property. In this case, if `value` has the `.text` property.

TypeScript knows that our if statement will only be `true` if `value` is a `Warning` because that's the only possible type for `value` that has a property called `.text`, so it narrows the type down to `Warning` inside the if block.

After the first, if statement, `value` can be `Warning | Error`. After the second if statement, it can only be `Error`.

## Equality Narrowing

It's also very common to support optional arguments, which means, allowing `value` to be `null` or `undefined`.

```ts
const formatErrorMessage =
  (value: null | undefined | string | Error | Warning): string => {
    const prefix = 'Error: ';

    // If it's null or undefined, return "Unknown" with the prefix
    if (???) {
      return prefix + 'Unknown'
    }

    // If it's a string, return the string with the prefix
    if (typeof value === 'string') {
      return prefix + value // <- value: string
    }

    // If it's a Warning, return the Warning.text with the prefix
    if ('text' in value) {
      return prefix + value.text // <- value: Warning
    }

    // If it's an Error, return the Error.message with the prefix
    return prefix + value.message // <- value: Error
}

interface Warning {
  text: string
}
```

We could handle the `undefined` case with the `typeof` operator but that wouldn't work with `null`.

By the way, if you want to know why it wouldn't work for `null` and the differences between `null` and `undefined`. I have a very short and informative article explaining just that. I'll leave a link for it in the references.

What we could do that would work for `null` and `undefined` is to use equality operators, such as `===`:

```ts
const formatErrorMessage = (
  value: null | undefined | string | Error | Warning
): string => {
  const prefix = 'Error: ';

  // If it's null or undefined, return "Unknown" with the prefix
  if (value === null || value === undefined) {
    return prefix + 'Unknown';
  }

  // If it's a string, return the string with the prefix
  if (typeof value === 'string') {
    return prefix + value;
  }

  // If it's a Warning, return the Warning.text with the prefix
  if ('text' in value) {
    return prefix + value.text;
  }

  // If it's an Error, return the Error.message with the prefix
  return prefix + value.message;
};

interface Warning {
  text: string;
}
```

Our if statement will only be `true` if `value` equals `null` or `undefined`, so TypeScript narrows our type to `null | undefined`.

That's called equality narrowing, and it also works with other comparison operators, such as:

- Not equals `!==`
- Loose equals `==`
- Loose not equals `!=`

## Truthiness Narrowing

But here's the thing. Equality narrowing is not the idiomatic JavaScript way of checking for `null | undefined`. The idiomatic way of doing this is to check if the value is truthy.

I have a short article explaining what is truthy and falsy in JavaScript. I'll put the link in the references. It would be nice if you could go watch that real quick so that we have the definition of truthy and falsy fresh in our minds. Go ahead, I'm waiting.

Now that we all have the definition of truthy and falsy fresh in our minds, let me introduce you to truthiness narrowing.

Instead of using equality narrowing to check if `value` equals `null` or `undefined`, we can just see if it's falsy.

```ts
const formatErrorMessage = (
  value: null | undefined | string | Error | Warning
): string => {
  const prefix = 'Error: ';

  // If it's falsy (null, undefined, empty string), return "Unknown" with the prefix
  if (!value) {
    return prefix + 'Unknown';
  }

  // If it's a string, return the string with the prefix
  if (typeof value === 'string') {
    return prefix + value;
  }

  // If it's a Warning, return the Warning.text with the prefix
  if ('text' in value) {
    return prefix + value.text;
  }

  // If it's an Error, return the Error.message with the prefix
  return prefix + value.message;
};

interface Warning {
  text: string;
}
```

We can do that by prefixing it with a logical NOT `!`. That will convert the value to a boolean and invert it. If it's falsy, it'll be converted to `false` and then inverted to `true`.

## Control Flow Analysis

So far, we've been avoiding a guard to check if `value` is an instance of the `Error` class. I told you how we're managing to do that. We are treating all the possible types so that there's only the `Error` type left in the end.

That technique is very common in JavaScript, and it's also a form of narrowing. The correct term for what we've been doing is "Control Flow Analysis".

Control flow analysis is the analysis of our code based on its reachability.

TypeScript knows that we can't _reach_ the first if statement unless `value` is truthy.

```ts
const formatErrorMessage = (
  value: null | undefined | string | Error | Warning
): string => {
  const prefix = 'Error: ';

  // If it's falsy (null, undefined, empty string), return "Unknown" with the prefix
  if (!value) {
    return prefix + 'Unknown';
  }

  // If it's a string, return the string with the prefix
  // if (typeof value === 'string') {
  // return prefix + value
  // }

  // If it's a Warning, return the Warning.text with the prefix
  // if ('text' in value) {
  // return prefix + value.text
  // }

  // If it's an Error, return the Error.message with the prefix
  return prefix + value.message;
};

interface Warning {
  text: string;
}
```

We can't reach the second if statement unless `value` is a `string`.

```ts
const formatErrorMessage = (
  value: null | undefined | string | Error | Warning
): string => {
  const prefix = 'Error: ';

  // If it's falsy (null, undefined, empty string), return "Unknown" with the prefix
  if (!value) {
    return prefix + 'Unknown';
  }

  // If it's a string, return the string with the prefix
  if (typeof value === 'string') {
    return prefix + value;
  }

  // If it's a Warning, return the Warning.text with the prefix
  // if ('text' in value) {
  // return prefix + value.text
  // }

  // If it's an Error, return the Error.message with the prefix
  return prefix + value.message;
};

interface Warning {
  text: string;
}
```

We can't reach the third if it's not a `Warning`. So in the end, there's only one type left, it can only be an `Error`.

```ts
const formatErrorMessage = (
  value: null | undefined | string | Error | Warning
): string => {
  const prefix = 'Error: ';

  // If it's falsy (null, undefined, empty string), return "Unknown" with the prefix
  if (!value) {
    return prefix + 'Unknown';
  }

  // If it's a string, return the string with the prefix
  if (typeof value === 'string') {
    return prefix + value;
  }

  // If it's a Warning, return the Warning.text with the prefix
  if ('text' in value) {
    return prefix + value.text;
  }

  // If it's an Error, return the Error.message with the prefix
  return prefix + value.message;
};

interface Warning {
  text: string;
}
```

Those types are being narrowed because TypeScript is using control flow analysis.

## The `instanceof` Operator Guard

But we don't need to rely on control flow analysis to narrow our type to `Error`. We can do it with a very simple and idiomatic JavaScript operator. The `instanceof` operator.

```ts
const formatErrorMessage = (
  value: null | undefined | string | Error | Warning
): string => {
  const prefix = 'Error: ';

  // If it's falsy (null, undefined, empty string), return "Unknown" with the prefix
  if (!value) {
    return prefix + 'Unknown';
  }

  // If it's a string, return the string with the prefix
  if (typeof value === 'string') {
    return prefix + value;
  }

  // If it's a Warning, return the Warning.text with the prefix
  if ('text' in value) {
    return prefix + value.text;
  }

  // If it's an Error, return the Error.message with the prefix
  if (value instanceof Error) {
    return prefix + value.message;
  }

  // We will never reach here
  throw new Error(`Invalid value type`);
};

interface Warning {
  text: string;
}
```

Here we are checking if `value` is an instance of the `Error` class, so TypeScript narrows our type down to `Error`. There is no type left after that last if statement, we will never reach any code that comes after it.

## Type `never` (15s)

If you're wondering what TypeScript considers to be the type of `value` after all of our if statements, the answer is `never`.

`never` is a special type that represents something impossible, something that should never happen.

## Conclusion

<p>Those were the fundamental type guards, they are super useful, but they will only take you so far. In the next articles, I'll show you how to create custom type guards. <app-newsletter-modal-trigger>Subscribe</app-newsletter-modal-trigger> if you don't want to miss it.</p>

References are below.

And if your company is looking for remote web developers, you can contact me and my team on [lucaspaganini.com](https://lucaspaganini.com).

As always, have a great day, and I'll see you soon!

## Related content

1. [1min JS - Falsy and Truthy](https://youtu.be/J3CTnNtsi_w)
2. [Null vs Undefined in JavaScript - Explained Visually](https://youtu.be/kiwqjqGwhGo)
3. [TypeScript Narrowing Part 1 - What is a Type Guard](https://youtu.be/EoPubc1u5IY)

## References

1. [TypeScript docs on narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html)
2. [TypeScript docs on the `never` type](https://www.typescriptlang.org/docs/handbook/basic-types.html#never)
3. [The `instanceof` operator on MDN](https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Operators/instanceof)
4. [The `typeof` operator on MDN](https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Operators/typeof)
5. [The `in` operator on MDN](https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Operators/in)