Assertion Functions or Assertion Guards - TypeScript Narrowing #5

TypeScript Narrowing #5

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.

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

LinkAssertion 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 strings, we return true if the given argument is a string and false if it's not.

TypeScript
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.

TypeScript
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.

TypeScript
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.

TypeScript
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.

LinkEarly 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.

TypeScript
/** 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.

TypeScript
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!

TypeScript
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 wanna know more about early exists, I have a one-minute video explaining this concept. The link is in the references.

TypeScript
/** 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');
}

LinkIssues 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
TypeScript
// Alternative 1: Functions Declaration

function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') throw Error('value is not a string');
}
  1. Use function expressions with predefined types
TypeScript
// 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');
};

LinkFunction 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.

TypeScript
// 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.

TypeScript
// 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:

TypeScript
// 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.

TypeScript
// 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.

LinkAssertions without a Type Predicate

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

TypeScript
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:

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

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

LinkConclusion

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.

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!

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

LinkReferences

  1. Assertion functions TypeScript Documentation
  2. Pull Request - Assertion Functions TypeScript Github Repository
  3. Pull Request - Error Messages for Assertion Functions that couldn't be Control Flow Analysed TypeScript Github Repository
  4. Issue - Assertion Functions and Function Expressions: TypeScript Github Repository
  5. Code Examples Lucas Paganini

Join the newsletter and be the first to know when I launch a course, post a video or write an article.

I don't SPAM, one email per week tops.