Asynchronous type guards
Tags: typescript, beginners, programming, tutorial
Published: 2022-06-14 12:15:21

TypeScript Narrowing #7

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

Hey, welcome to part 7 of our TypeScript narrowing series.

Before we start, let me say this: It is recommended that you read the articles of this series in sequence, but you don't really need to read _all_ of them to understand this one. What you do need, is to read part 3 and 6, at least. Otherwise, you'll probably feel lost here. Ok?

Today, we’ll talk about a highly requested feature in TypeScript: asynchronous type guards!

We just have one small problem… it’s still just a feature _request_, there’s no official support for asynchronous type guards yet.

But as developers, we can’t just wait for someone else to fix our problems, sometimes (most times) we need to find a solution ourselves, and with the tools we currently have available.

And that’s exactly what we’re going to do. I’ll show you the cleanest workaround for asynchronous type guards that I came up with. It is also a solution that will be easy to refactor once we do get support for asynchronous type guards in TypeScript.

If you have a different solution, please, leave a comment and we can discuss it.

I'm Lucas Paganini, and in this blog, we release web development tutorials. Subscribe if you're interested in that.

## The Goal

In an ideal world, we would be writing asynchronous functions that return a type predicate wrapped in a `Promise`.

```ts
const isStringAsync =
  async (value: unknown): Promise<value is string> =>
    typeof value === "string"
```

We can’t do that yet, but we can strive to get as close to it as possible. So, let's look for a workaround that is close to this ideal implementation.

Also, you might be wondering why would we ever asynchronously check if a value is a `string`. Well, I too wonder that. Why _would_ we ever do that?

The thing is, I have a real world use case where asynchronous type guards would be useful. And I’ll show you that scenario. But I want us to first understand our implementation of asynchronous type guards. So let’s stick with our fictional `isStringAsync` function for now.

## The Workaround

We can't currently return a type predicate wrapped in a `Promise`, but we can return a `boolean` wrapped in a `Promise`.

```ts
const isStringAsync =
  async (value: unknown): Promise<value is string> =>
    typeof value === "string"
```

```ts
const isStringAsync = async (value: unknown): Promise<boolean> =>
  typeof value === 'string';
```

Now, as you might have guessed, that `boolean` means nothing to TypeScript. It performs no type narrowing in our variable.

```ts
const isStringAsync = async (value: unknown): Promise<boolean> =>
  typeof value === 'string';

const aaa = 1 as number | string | Date;
isStringAsync(aaa).then((isString) => {
  if (isString) {
    aaa; // <- aaa: string | number | Date
  } else {
    aaa; // <- aaa: string | number | Date
  }
});
```

That's sad. It seems like we can only have _synchronous_ type guards...

But wait. In the last article, we learned that we can have functions that create new type guards. So maybe we could have a function that _asynchronously_ creates a _synchronous_ type guard.

See, instead of returning the `Promise` of a type predicate, which we can't do yet.

```ts
const isStringAsync =
  async (value: unknown): Promise<value is string> =>
    typeof value === "string"
```

We could return the `Promise` of a function that returns a type predicate.

```ts
const isStringAsync =
  async (value: unknown): Promise<(v: unknown) => v is string> =>
  (v): v is string =>
    typeof value === 'string';
```

And it actually works!

```ts
const isStringAsync =
  async (value: unknown): Promise<(v: unknown) => v is string> =>
  (v): v is string =>
    typeof value === 'string';

const aaa = 1 as number | string | Date;
isStringAsync(aaa).then((isString) => {
  if (isString(aaa)) {
    aaa; // <- aaa: string
  } else {
    aaa; // <- aaa: number | Date
  }
});
```

And it's so close to the ideal scenario, that it will be very easy to refactor our code once we do get native support for asynchronous type guards.

```ts
const isStringAsync =
  async (value: unknown): Promise<v is string> =>
    typeof value === "string"

const aaa = 1 as number | string | Date
isStringAsync(aaa).then(isString => {
  if (isString) {
    aaa // <- aaa: string
  } else {
    aaa // <- aaa: number | Date
  }
})
```

## Motivations for Asynchronous Type Guards

Awesome! Now that we got the implementation, let's discuss a real world scenario where it would _really_ be beneficial for us to have asynchronous type guards. Because checking if a value is a `string` asynchronously, is not a good example.

I'll give you an example inspired by the one that Dominik Głodek gave when he made the feature request for asynchronous type guards:

You're writing an API endpoint that receives data to create a user. The user data consists of an email and a password.

```ts
interface User {
  email: string;
  password: string;
}
```

You can only save users to the database if they are valid. For a user to be valid, it needs to meet 2 criteria:

1. The password should have at least 8 characters
2. The email cannot already belong to another user

The first check could be done synchronously, so let's start with that. We will create an assertion function called `validateUser` that checks that our object complies with the `User` interface and with the password validation.

```ts
type ValidateUser = (value: unknown) => asserts value is User;

const validateUser: ValidateUser = (value) => {
  assertHasProperties(['email', 'password'], value);
  assertIsString(value.email);
  assertIsString(value.password);

  // 1. The password should have at least 8 characters
  if (value.password.length < 8) throw Error('Password is too short');
};
```

👉 If you don't know what an assertion function is, we have a full article on this topic. It's the fifth article of this series. I'll leave a link for it in the references.

Before we add our asynchronous validation to make sure the user email doesn't already belong to another user, let's see what would happen if we tried to use our assertion function as it is right now.

```ts
const saveUserToDatabase =
  async (user: User): Promise<void> => { ... }

type ValidateUser =
  (value: unknown) =>
    asserts value is User
const validateUser: ValidateUser = value => { ... }

let user: unknown
validateUser(user)
await saveUserToDatabase(user)
```

As you can see, TypeScript thinks it's all good. There are no compilation errors. `saveUserToDatabase` is expecting a `User` and that's what it's getting. But it doesn't know that this `User` has not been fully validated yet.

Actually, TypeScript would think it's all good even if we did not validate the password length. In short, TypeScript is only verifying that our value complies with the `User` interface, it's not verifying if it passes the validation criteria.

Could we somehow tell TypeScript when a value not only complies with the `User` interface but is also a fully validated user? Well... yeah. We can.

We can create a secret property to indicate that the password length has been validated and another to indicate that the email doesn't belong to another user. Also, we can have a union type, called `ValidatedUser` which is equal to an `User` that has both validations.

```ts
type PasswordValidated<T> = T & {
  readonly __passwordValidated__: unique symbol;
};

type UniqueEmailValidated<T> = T & {
  readonly __uniqueEmailValidated__: unique symbol;
};

type ValidatedUser = PasswordValidated<User> & UniqueEmailValidated<User>;
```

With that in place, we can change the signature of `validateUser` to assert that the value is not just a `User`, but a `PasswordValidated<User>`.

```ts
type ValidateUser =
  (value: unknown) =>
    asserts value is User
const validateUser: ValidateUser = value => { ... }
```

```ts
type ValidateUser =
  (value: unknown) =>
    asserts value is PasswordValidated<User>
const validateUser: ValidateUser = value => { ... }
```

And we can also change the signature of `saveUserToDatabase` to make it only accept users that have already been fully validated.

```ts
type ValidateUser =
  (value: unknown) =>
    asserts value is PasswordValidated<User>
const validateUser: ValidateUser = value => { ... }

const saveUserToDatabase =
  async (validUser: ValidatedUser): Promise<void> => { ... }
```

With that structure in place, I'm weirdly happy to say that our code would NOT compile.

```ts
type PasswordValidated<T> = T & {
  readonly __passwordValidated__: unique symbol
}

type UniqueEmailValidated<T> = T & {
  readonly __uniqueEmailValidated__: unique symbol
}

type ValidatedUser = PasswordValidated<User> & UniqueEmailValidated<User>

const validateUser =
  (value: User): asserts value is PasswordValidated<User> => { ... }

const saveUserToDatabase =
  async (validUser: ValidatedUser): Promise<void> => { ... }

let user: unknown
validateUser(user)
await saveUserToDatabase(user)
// Compilation error: Argument of type 'PasswordValidated<User>' is not assignable to parameter of type 'ValidatedUser'.
```

Yaaaay 🎉🎉🎉

For it to compile again, we need to create an asynchronous assertion function using the same trick that we used for asynchronous type guards.

```ts
type ValidateUser = (
  value: unknown
) => asserts value is PasswordValidated<User> & UniqueEmailValidated<User>;

const validateUserAsync = async (value: unknown): Promise<ValidateUser> => {
  // If we throw an error, save it to throw later, in the assertion function
  let errorToThrow: Error | null = null;

  try {
    assertHasProperties(['email', 'password'], value);
    assertIsString(value.email);
    assertIsString(value.password);

    // 1. The password should have at least 8 characters
    if (value.password.length < 8) throw Error('Password is too short');

    // 2. The email cannot already belong to another user
    if (await emailIsAlreadyTaken(value.email))
      throw Error('Email is already taken');
  } catch (error) {
    errorToThrow = error;
  }

  return (v) => {
    if (errorToThrow) throw errorToThrow;
  };
};
```

Ok, I think we're good now. Calling `validateUserAsync` returns the `Promise` of a synchronous assertion function. Which we then use to assert that the value is a `ValidatedUser`.

```ts
type PasswordValidated<T> = T & {
  readonly __passwordValidated__: unique symbol
}

type UniqueEmailValidated<T> = T & {
  readonly __uniqueEmailValidated__: unique symbol
}

type ValidatedUser = PasswordValidated<User> & UniqueEmailValidated<User>

type ValidateUser =
  (value: unknown) =>
    asserts value is
      PasswordValidated<User> & UniqueEmailValidated<User>
const validateUserAsync =
  async (value: unknown): Promise<ValidateUser> => { ... }

const saveUserToDatabase =
  async (validUser: ValidatedUser): Promise<void> => { ... }

let user: unknown
const validateUser: ValidateUser = await validateUserAsync(user)
validateUser(user)
await saveUserToDatabase(user)
```

Cool, now we're protecting ourselves from performing database operations with unvalidated resources. That's a real world use case for asynchronous type guards and a whole bunch of advanced TypeScript madness.

### `makeAsyncPredicateFunction`

And you know what? I love you. So I went ahead and made our lives even easier.

I made an asynchronous higher order guard that creates asynchronous type guards for us. If you choose to use my utility function, it will be even easier to create asynchronous guards and refactor them once we get native support for them in TypeScript.

```ts
import { makeAsyncPredicateFunction } from '@lucaspaganini/ts';

const isStringAsync = makeAsyncPredicateFunction<string>(
  async (value) => typeof value === 'string'
);

const aaa = 1 as number | string | Date;
isStringAsync(aaa).then((isString) => {
  if (isString(aaa)) {
    aaa; // <- aaa: string
  } else {
    aaa; // <- aaa: number | Date
  }
});
```

It works on Node and Browsers, you can install it with `npm install @lucaspaganini/ts`. But that library is a topic for the next article, so stay tuned.

## Conclusion

References are in the description.

I made a comment in the feature request for asynchronous type guards, explaining the workaround I came up with. If you don't mind, it would be great if you give a thumbs up to the feature request and to my comment, to give them more traction. The link for it is also in the references.

If you enjoyed the content, you know what to do.

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

In the next article, we'll talk about that library. And I'm planning it to be the last article of this series, so stick around, I think it'll be worth it.

Until then, have a great day, and I’ll see you soon.

## References

1. [Feature Request - Asynchronous Type Guards and Assertion Signatures _TypeScript GitHub Repository_](https://github.com/microsoft/TypeScript/issues/37681)
2. [My Comment Explaining this Workaround _TypeScript GitHub Repository_](https://github.com/microsoft/TypeScript/issues/37681#issuecomment-975896901)
3. [TypeScript Utilities Library - `@lucaspaganini/ts` _GitHub Repository_](https://github.com/LucasPaganini/ts)