Typescript Type Assertions

Type assertions look a lot like of type guards, with the exception that they don't need to be embedded in an if statement.

Imagine we have a blog, and allow authenticated users to post comments. We come up with a function like this:

function isAuthenticated(user: User | null) {
  return typeof user !== null;
}

function addComment(user: User | null, comment: string) {
  if (!isAuthenticated(user)) {
    throw new Error('unauthenticated')
  }

  db.comments.insert({ author: user._id, comment });
}

We have extracted an isAuthenticated helper that tells us if the user is logged in. And we make sure to throw an error if they are not.

In pure JavaScript, we would be done by now. An error will be thrown if user is null, so by the time we reach the database statement, we're sure that the user object is defined.

TypeScript on the other hand, still sees the user as User | null. To fix that, we can introduce a type guard. Update the helper by adding the type predicate user is User , and it understands that user is null in the scope of the if statement, and thereby has to be User after it.

function isAuthenticated(user: User | null): user is User {
  return typeof user !== null;
}

I recommend reading my earlier article about Type guards and Type predicates if you're unfamiliar with those.

By adding the type predicate we've fixed the issue in the database statement. TypeScript is aware that user will never be null at that stage.

Assertion Functions

The problem lies in repetition. Having those 3 lines of code all around the project, adds noise. Besides, it's unlikely that this is the only check you have defined.

We could turn that check into something like:

function assertAuthenticated(user: User | null): user is User {
  if (user === null || user === undefined) {
    throw new Error('unauthenticated');
  }

  return true;
}

function addComment(user: User | null, comment: string) {
  assertAuthenticated(user);

  db.comments.insert({ author: user._id, comment });
}

In plain JavaScript, that would still work. The assertAuthenticated function throws if the user object is not defined, and because the error propagates, we never reach the database statement.

However, because we removed the wrapping if statement, TypeScript is again not happy. The user._id in the database statement, will throw an TS2531: Object is possibly 'null'. To fix that, we'll insert the Type assertion.

It's quite trivial, really. Simply add asserts in front of the type predicate, and remove the return statement from the assertion function. Where type guards must return a boolean, assertion functions must return void.

function assertAuthenticated(user: User | null): asserts user is User {
  if (user === null || user === undefined) {
    throw new Error('unauthenticated');
  }
}

And now when you call this function, TypeScript knows that the value of user can never be null after that line.

function addComment(user: User | null, comment: string) {
  assertAuthenticated(user);
  db.comments.insert({ author: user._id, comment });
}

Assert Generic

Now that we know about asserts, we can also quite easily introduce a reusable helper function:

function assert<T>(
  condition: T,
  message,
): asserts condition is Exclude<T, null | undefined> {
  if (condition === null || condition === undefined) {
    throw new Error(message);
  }
}

And then whenever you have a function that accepts optional or partial values, you can simply protect them using a this assert helper.

async function latestBlog() {
  const blog = await db.blogs.findOne({ author: 'smeijer' }); // Blog | null
  assert(blog, 'author does  not have any blogs');
    
  // and here we know `blog: Blog`
}

When the blog is not found in the database, the assert statement will throw an error up the chain, if it did find something, we can safely work with the object after the assert call.

The next time you consider type casting a property with as MyType, consider writing an type assertion instead. Instead of simply silencing TypeScript, you'll get runtime validation with a single line of code.

Liked this article?

If you made it to here, please share your thoughts on BlueSky, or leave a comment below.