Custom Type Guards
Tags: typescript, programming, tutorial, beginners
Published: 2022-06-10 21:14:25

TypeScript Narrowing #3

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

Hey, welcome to another article in our TypeScript Narrowing series. In this article, I'll explain:

1. Type predicates
2. How to create your own guards
3. How to create a guard by exclusion

This is the third article in our series, if you haven't watched the previous ones, I highly recommend that you do, they lay out solid fundamentals for narrowing. I'll leave a link for them in the references.

<p>I'm Lucas Paganini, and on this site, we release web development tutorials. If that's something you're interested in, leave a like and <app-newsletter-modal-trigger>subscribe to the newsletter</app-newsletter-modal-trigger>.</p>

## Type Predicates

In the last article, we explored the fundamental type guard operators. Now I'd like to show you type guard functions.

For example, if you need to check if a variable called `value` is a `string`, you could use the `typeof` operator. But what you could also do, is to create a function called `isString()` that receives an argument and returns `true` if the given argument is a `string`.

```ts
const isString = (value: any): boolean => typeof value === 'string';
```

Remember our `formatErrorMessage()` function from the last article?

```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;
}
```

Let's remove the `typeof` operator from it and use `isString()` instead.

```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 (isString(value)) {
    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;
}
```

It's the same code, we've just isolated the guard in a function, right? No. It breaks. TypeScript is not narrowing the type to `string`, the guard is not working.

Here's the thing, `isString()` is returning a `boolean` and we know what that `boolean` means.

```ts
const isString = (value: any): boolean => typeof value === 'string';
```

It means that the argument is a `string`. But TypeScript doesn't know what that `boolean` means, so let's teach it.

Instead of saying that our function returns a `boolean`, we need to say that our function returns the answer to the question: "is this argument a `string`?".

Given that the name of our argument is `value`, we do that with the following syntax: `value is string`.

```ts
const isString = (value: any): value is string => typeof value === 'string';
```

Now TypeScript understands that `isString()` is a type guard and our `formatErrorMessage()` function compiles correctly.

The return type of our `isString()` function is not just a `boolean` anymore, it's a "Type Predicate".

So to make a custom type guard, you just define a function that returns a type predicate.

All type predicates take the form of `{ parameter } is { Type }`.

## The `unknown` Type

A quick tip before we continue:

Instead of using the type `any` in our custom guard parameter, our code would be safer if we used the `unknown` type.

```ts
const isString = (value: unknown): value is string => typeof value === 'string';
```

I made a one-minute video explaining the differences between `any` and `unknown`, the link is in the references.

## Custom Guards

Let's exercise our knowledge by converting all the checks in our `formatErrorMessage()` function to custom guards.

We already have a guard for `string`s, now we need guards for `Warning`, `Error` and falsy types.

### `Error` Guard

The guard for `Error` is pretty straightforward, we just isolate the `instanceof` operator check in a function.

```ts
const isError = (value: unknown): value is Error => value instanceof Error;
```

### `Warning` Guard

But the `Warning` guard, on the other hand, is not that simple.

TypeScript allowed us to use the `in` operator because there was a limited amount of types that our `value` parameter could be, and they were all objects.

```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 (isString(value)) {
    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 (isError(value)) {
    return prefix + value.message;
  }

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

interface Warning {
  text: string;
}
```

But if we create a function and say that our parameter is `unknown`, then it could be anything. Including primitive types, and that would throw an `Error` because we can only use the `in` operator in objects.

```ts
interface Warning {
  text: string;
}

const isWarning = (value: unknown): value is Warning => 'text' in value; // Compilation error
```

The solution is to make sure our parameter is a valid object before using the `in` operator. And we also need to make sure that it's not `null`.

```ts
interface Warning {
  text: string;
}

const isWarning = (value: unknown): value is Warning =>
  typeof value === 'object' && value !== null && 'text' in value;
```

### Falsy Guard

For the falsy values guard, we first need to define a type with values that are considered falsy.

```ts
type Falsy = false | 0 | -0 | 0n | '' | null | undefined;
```

I'm not including `NaN` here because there is no `NaN` type in TypeScript.

```ts
type Falsy = false | 0 | -0 | 0n | '' | null | undefined | ~~NaN~~;
```

The type of `NaN` is `number` and not all numbers are falsy, so that's why we're not dealing with `NaN`.

```ts
typeof NaN;
//=> number
```

There is a proposal to add `NaN` as a type – and also `integer`, `float` and `Infinity`. I think that's nice, it would be helpful to have those types.

```ts
// Proposal
type number = integer | float | NaN | Infinity;
```

I'll leave a link for the proposal in the references.

Anyway, now that we have our `Falsy` type, we can create a falsy values guard.

Remember, a value is falsy if it's considered `false` when converted to a `boolean`. So, to check if our `value` is falsy, we can use abstract equality to see if it gets converted to `false`.

```ts
type Falsy = false | 0 | -0 | 0n | '' | null | undefined;

const isFalsy = (value: unknown): value is Falsy => value == false;
```

## `formatErrorMessage()` with Custom Guards

That's it, we now have all the custom guards that we need for our `formatErrorMessage()` function.

```ts
// FUNCTION
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 (isFalsy(value)) {
    return prefix + 'Unknown';
  }

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

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

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

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

```ts
// GUARDS
const isString = (value: unknown): value is string => typeof value === 'string';

const isError = (value: unknown): value is Error => value instanceof Error;

interface Warning {
  text: string;
}

const isWarning = (value: unknown): value is Warning =>
  typeof value === 'object' && value !== null && 'text' in value;

type Falsy = false | 0 | -0 | 0n | '' | null | undefined;

const isFalsy = (value: unknown): value is Falsy => value == false;
```

## BONUS: Narrowing by Exclusion

Before we wrap this up, I want to show you something.

There is a limited list of falsy values, right?

`````markdown
1. `false`
2. `0` `-0` `0n` representations of zero
3. ```` `""` `''` empty string
4. `null`
5. `undefined`
6. `NaN` not a number
`````

But truthy values, on the other hand, are infinity. All values that are not falsy, are truthy.

So, how would make a type guard for truthy values?

### Truthy Guard

The trick is to exclude the falsy types.

Instead of checking if our value is truthy, we check that it's **_not_** falsy.

```ts
type Truthy<T> = Exclude<T, Falsy>;

const isTruthy = <T extends unknown>(value: T): value is Truthy<T> =>
  value == true;

// Test
const x = 'abc' as null | string | 0;
if (isTruthy(x)) {
  x.trim(); // `x: string`
}
```

I use this trick a lot, and we'll see it again in future articles.

## Conclusion

References and other links are below.

If you haven't already, please like, <app-newsletter-modal-trigger>subscribe</app-newsletter-modal-trigger>, and follow us on social media. This helps us grow, which results in more free content for you. It's a win-win.

And if your company is looking for remote web developers, I and my team are currently available for new projects. You can contact us on [lucaspaganini.com](https://lucaspaganini.com).

Have a great day, and I'll see you soon.

## Related content

1. [1min JS - Falsy and Truthy](https://youtu.be/J3CTnNtsi_w)
2. [1min TS - Unknown vs Any](https://youtu.be/k_q-NnmwGTo)
3. [TypeScript Narrowing Series](https://www.youtube.com/watch?v=EoPubc1u5IY&list=PLLbNB9i2QOKMgZN0qkSaXqq1SIRs-mU7X)
4. [TypeScript Narrowing Part 1 - What is a Type Guard](https://youtu.be/EoPubc1u5IY)
5. [TypeScript Narrowing Part 2 - Type Guard Operators](https://youtu.be/ZdMfiVK7fcM)

## References

1. [TypeScript docs on narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html)
2. [TypeScript docs type predicates](https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates)
3. [The `in` operator on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/in)
4. [TypeScript proposal to add `NaN` as a type](https://github.com/Microsoft/TypeScript/issues/28682)