Assertion Functions or Assertion Guards
Tags: typescript, beginners, programming, tutorial
Published: 2022-06-10 21:15:09

TypeScript Narrowing #5

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

Welcome to the fifth article in our TypeScript narrowing series! Be sure to read the previous ones, if you haven't yet. Their links are in the references.

In this article, I'll show you assertion functions, also known as assertion _guards_.

<p>I'm Lucas Paganini, and on this website, we release web development tutorials. <app-newsletter-modal-trigger>Subscribe</app-newsletter-modal-trigger> if you're interested in that.</p>

## Assertion Functions vs Type Guards

The reason why assertion functions are also known as assertion _guards_ is because of their similarity to type guards.

In our type guard for `string`s, we return `true` if the given argument is a `string` and `false` if it's not.

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

If we wanted an assertion function instead of a type guard, instead of returning either `true` or `false`, our function would either `return` or `throw`. If it is a `string`, it returns. If it's not a `string`, it throws.

```ts
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') throw Error('value is not a string');
}
```

If you call a function that throws if your value is not a `string`, then all the code that comes after it will only run if your value _is_ a `string`, so TypeScript narrows our type to `string`.

```ts
const x = 'abc' as string | number;
x; // <- x: `string | number`

assertIsString(x);
x; // <- x: `string`
```

To abstract this explanation: TypeScript uses control flow analysis to narrow our type to what was asserted. In this case, we have asserted that `value` is a `string`.

```ts
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') throw Error('value is not a string');
}
```

We talk about control flow analysis in the second article of this series, the link for it is in the references.

## Early Exits

Now, _when_ and _why_ would you want to use an assertion function instead of a type guard?

Well, the most popular use case for assertion functions is data validation.

Suppose you have a NodeJS server, and you’re writing a handler for user creation.

```ts
/** Expected body for POST /api/users */
interface CreatableUser {
  name: string;
  email: string;
  password: string;
}
```

The first thing you should do in your request handlers is to validate the data. If any of the fields are missing or invalid, you’ll want to throw an error.

```ts
function assertIsCreatableUser(value: unknown): asserts value is CreatableUser {
  if (typeof value !== 'object') throw Error('Creatable user is not an object');
  if (value === null) throw Error('Creatable user is null');

  assertHasProps(['name', 'email', 'password'], value);
  assertIsName(value.name);
  assertIsEmail(value.email);
  assertIsPassword(value.password);
}
```

When you have conditions to check at the beginning of your code, and you refuse to run if those conditions are invalid, that’s called an “early exit” and it’s the perfect scenario for an assertion function!

```ts
const userCreationHandler = (req: Request, res: Response): void => {
  try {
    // Validate the data before anything
    const data = req.body
    assertIsCreatableUser(data)

    // Data is valid, create the user
    ...
  } catch (err) {
    // Data is invalid, respond with 400 Bad Request
    const errorMessage =
      err instanceof Error
        ? err.message
        : "Unknown error"
    res.status(400).json({ errors: [{ message: errorMessage }] })
  }
}
```

If you want to know more about early exits, I have a one-minute video explaining this concept. The link is in the references.

```ts
/** Non empty string between 3 and 256 chars */
type Name = string;

function assertIsName(value: unknown): asserts value is Name {
  if (typeof value !== 'string') throw Error('Name is not a string');
  if (value.trim() === '') throw Error('Name is empty');
  if (value.length < 3) throw Error('Name is too short');
  if (value.length > 256) throw Error('Name is too long');
}
```

## Issues with Control Flow Analysis

Maybe you've noticed that I'm using function declarations instead of function expressions. There's a reason for that.

First, knowing the differences between function declarations and functions expressions is very important. I'm sure most of you already know that, but if you don't, it's ok. I'll leave a link in the references for a one-minute video explaining their differences.

So, back to assertion functions. I'm using function declarations because TypeScript has trouble recognizing assertions functions during control flow analysis if they're written as function expressions.

Function declarations work because they're hoisted, so their types are declared previously and TypeScript likes that.

I think that's a bug. But I don't know if they're going to fix this. Currently, they say it's working as intended.

To work around that issue, I've found two alternatives:

1. Use function declarations

```ts
// Alternative 1: Functions Declaration

function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') throw Error('value is not a string');
}
```

2. Use function expressions with predefined types

```ts
// Alternative 2: Function Expressions with Predefined Types

// Predefined type
type AssertIsString = (value: unknown) => asserts value is string;

// Function expression with predefined type
const assertIsString: AssertIsString = (value) => {
  if (typeof value !== 'string') throw Error('value is not a string');
};
```

## Function Expressions with Predefined Types

I prefer function expressions, so I'll go with the second alternative.

For that, instead of defining our function signature along with its implementation.

```ts
// DON'T: Signature with implementation
const assertIsString = (value: unknown): asserts value is string => {
  if (typeof value !== 'string') throw Error('value is not a string');
};
```

We'll have to define its signature as an isolated type and cast our function to that type.

```ts
// Predefined type
type AssertIsString = (value: unknown) => asserts value is string;

// Function expression with predefined type
const assertIsString: AssertIsString = (value) => {
  if (typeof value !== 'string') throw Error('value is not a string');
};
```

Here's how it looks like if we wanted to use function expressions for our `assertIsName` function:

```ts
// Predefined type
type AssertIsName = (value: unknown) => asserts value is Name;

// Function expression with predefined type
const assertIsName: AssertIsName = (value) => {
  if (typeof value !== 'string') throw Error('Name is not a string');
  if (value.trim() === '') throw Error('Name is empty');
  if (value.length < 3) throw Error('Name is too short');
  if (value.length > 256) throw Error('Name is too long');
};
```

And we were also using an `assertHasProps` function to check that our object has the properties that we expect. Maybe you're curious, so I'm showing it too because I think that function has an interesting signature.

```ts
// Predefined type
type AssertHasProps = <Prop extends string>(
  props: ReadonlyArray<Prop>,
  value: object
) => asserts value is Record<Prop, unknown>;

// Function expression with predefined type
const assertHasProps: AssertHasProps = (props, value) => {
  // Only objects have properties
  if (typeof value !== 'object') throw Error(`Value is not an object`);

  // Make sure it's not null
  if (value === null) {
    throw Error('Value is null');
  }

  // Check if it has the expected properties
  for (const prop of props)
    if (prop in value === false) throw Error(`Value doesn't have .${prop}`);
};
```

I'm also leaving a link to the GitHub issues and PRs related to this if you want to know more.

## Assertions without a Type Predicate

Before we wrap this up, I want to show you a different signature for assertion functions:

```ts
type Assert = (condition: unknown) => asserts condition;
const assert: Assert = (condition) => {
  if (condition == false) throw 'Invalid assertion';
};
```

This signature is weird, right? There is no type of predicate, what the hell are we asserting?

This signature means that the condition to check is already a type guard. For example, you could give it a `typeof` expression, and it would narrow the type accordingly:

```ts
const x = 'abc' as string | number;
x; // <- x: `string | number`

assert(typeof x === 'string');
x; // <- x: `string`
```

## Conclusion

Assertion functions are cool, right? People are just not used to them yet.

References are below. 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/).

This is not the last article of this series. We have more to come! Until then, have a great day, and I’ll see you in the next one!

## Related Content

1. 1m JS: Early Exits
2. TypeScript Narrowing pt. 1 - 8

## References

1. [Assertion functions _TypeScript Documentation_](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions)
2. [Pull Request - Assertion Functions _TypeScript GitHub Repository_](https://github.com/microsoft/TypeScript/pull/32695)
3. [Pull Request - Error Messages for Assertion Functions that couldn't be Control Flow Analysed _TypeScript GitHub Repository_](https://github.com/microsoft/TypeScript/pull/33622)
4. [Issue - Assertion Functions and Function Expressions: _TypeScript GitHub Repository_ ](https://github.com/microsoft/TypeScript/issues/34523)
5. [Code Examples _Lucas Paganini_](https://www.typescriptlang.org/play?#code/LAKFHoCpIAgOQPYDsYFMC2AHALgTxgM7YBOAlkgOYwBGq2A7qqigMwwCGSAJjAEwCsANhgBjABbtiBGJHCg8mVPHbolAXkIlyFUPNyKYAQQIFUxbAEkCcFepgAKAG7sANgFdUALhhukAayQEeiQAShg1AD4OEzNsaWd3JVJpG1VQEWQiaNNzK1SvYxzLa1twmASPcKiAb1AYGFIAMwcFVARmiqUAQjUNACIiMko+sLr6mGwxYiCYAFFiaeJ7PvyG6UDsDk0hihGxpodOgDotdHsw3v69kHGJqZn5xeXV5LQsPGv6g6dXDyOXZgUSYwAA8MBYITG9Um03ocwWCCWK1Kr2wCAQhDEiOwnwazR+iX+gOBUQEgkhN3GMIeCKRL2kaIxLmQuwpAF9dCBWkYYuYABLsAgABWmmGkGhBIoQmDQAA9sMwuNJBtoIvZMKKCN5DAt2LhJaKIgAacq-LwwBDUABWqBE2AuUUFRXiZrWMAASrbEVwDdKTb4AkEkBFQKhZZhsaJMpsnbEBcLNdredh41KxWV1ZqTZ0HTBapTvp1wr0YEg3C4XBTbtS4Y9EcsAGqu15livXMaNREODJILIa6UW5r9sWjSlffGZgfkU2JC4lxquUxV253WHwp4AAybiRgXAQqAISAA5JsJI4lEcACTVYdsjfszmcqCwWbh20KnjUBBcfCd4gwIUAHkAGUABUYHAdhMFIcA3ByaRZFAcgFWIBcRCUABhYhUHYbB2GoAEAFUcjzMYkFsbwVUoMYMHYUgXEorRqMpTAnXob1GJ2UAOTALl9CUQpYisLCcLwgjUGIswM06bwA0CYJc1jcwXR3V4RNw-CiJI9Jo2yISCHUsStLMJMimE7CNPEyT-w0ItIlIgt8VadoZ0qHp+ktG07VxaF7lrWllkMzSlDgqSWwQGMUE899cULV1LlLctKyhVcaSePogvEnwSJbJLcTGJSU0FNMCHsABtI9yNUI8TSPWj6JqmAj1Ykx2OILgjwAXWzM1l0KvJbAJP4qtQB9eJ7LJQuIQzSGQAVuABGyHGwgBHbxPRWjwiBNbCtQ9A8I17UbvEcBBSB4ez82hYh8Cu8ZwHAGBt3O3ClEmJQvx-GhUD-JROFwSZtBSibNk+-ANFWo4wZS-qDIsoyJJyewwYpFKHpgAARDS3QSc6TREeG3rEELtLHGhv1wI4RpSsGjnqlwaYpo4WoINquDGNlRFw8QHDMYgwju+p0axvC3XIXGuB2g7kB4ehSGBAAWAAGJWYAAIXYHgNq27Bgd0vnEQAWQPAh2AoOwUvqPmGl7PCkHQly62IS3xgAfjQBYjlUEwzdQF36m8PpCP8eSUAN4g+hS3ajiIXC4PsZWlZCI4rQIZB7GqD3Fj2srM+903ze8cPjZ982YDZTry+XHi2SAA)