DEV Community

Cover image for Typescript Type Assertions
Stephan Meijer
Stephan Meijer

Posted on • Edited on • Originally published at meijer.ws

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 });
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

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');
  }
}
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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`
}
Enter fullscreen mode Exit fullscreen mode

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.


๐Ÿ‘‹ I'm Stephan, and I'm building metricmouse.com. If you wish to read more of mine, follow me on Twitter.

Top comments (6)

Collapse
 
larsejaas profile image
Lars Ejaas

Stephan, this is so useful! Thanks a lot, I think this is THE most useful post I have actually read her on dev.to ๐Ÿ‘ŒWorking with data from a headless CMS I have always felt there must be a more elegant alternative to all those typeguards!

Collapse
 
smeijer profile image
Stephan Meijer

Thanks Lars! Your kind words are appreciated. ๐Ÿ˜Š

Collapse
 
scooperdev profile image
Stephen Cooper

Thanks for sharing this! Very helpful.

Collapse
 
aboudard profile image
Alain Boudard

Whoah, I'm totally amazed. You really made me step up my Types game !
Article is short enough, with all the code for us to play with ...
Thank you very much ! ๐Ÿ‘Œ

Collapse
 
herobank110 profile image
David Kanekanian

awesome
can you have multiple asserts?

Collapse
 
lamine105 profile image
lamine

Really useful and practical tip for dealing with optional values.
Great post.